#include #include #include #include #include #include #ifdef _WIN32 #include #include #include #else // UNIX #ifndef GUI #include #include #include #else // UNIX and GUI #include #include #endif // GUI #endif // UNIX #include "filesystem.hpp" #include "functions.hpp" #ifdef _WIN32 #define cout std::wcout #define cerr std::wcerr #define cin std::wcin constexpr const char_t *dir_divider = L"\\"; #else // UNIX #define TEXT( a ) a #define cout std::cout #define cerr std::cerr #define cin std::cin constexpr const char_t *dir_divider = "/"; #endif // UNIX #ifndef GUI constexpr std::array< const char_t *, 46 > languages{ TEXT( "en" ), TEXT( "English" ), TEXT( "sv" ), TEXT( "Svenska" ), TEXT( "no" ), TEXT( "Norsk" ), TEXT( "da" ), TEXT( "Dansk" ), TEXT( "fi" ), TEXT( "Suomeksi" ), TEXT( "nl" ), TEXT( "Nederlands" ), TEXT( "de" ), TEXT( "Deutsch" ), TEXT( "it" ), TEXT( "Italiano" ), TEXT( "es" ), TEXT( "Español" ), TEXT( "fr" ), TEXT( "Français" ), TEXT( "pl" ), TEXT( "Polski" ), TEXT( "hu" ), TEXT( "Magyar" ), TEXT( "el" ), TEXT( "Greek" ), TEXT( "tr" ), TEXT( "Turkish" ), TEXT( "ru" ), TEXT( "Russian" ), TEXT( "he" ), TEXT( "Hebrew" ), TEXT( "ja" ), TEXT( "Japanese" ), TEXT( "pt" ), TEXT( "Portuguese" ), TEXT( "zh" ), TEXT( "Chinese" ), TEXT( "cs" ), TEXT( "Czech" ), TEXT( "sl" ), TEXT( "Slovenian" ), TEXT( "hr" ), TEXT( "Croatian" ), TEXT( "ko" ), TEXT( "Korea" ) }; #endif // not GUI #ifdef _WIN32 // functions to convert between string and wstring std::string wstring_to_utf8( const std::wstring &wstring ) { std::wstring_convert< std::codecvt_utf8_utf16< wchar_t > > wconv; return wconv.to_bytes( wstring ); } std::wstring utf8_to_wstring( const std::string &utf8 ) { std::wstring_convert< std::codecvt_utf8_utf16< wchar_t > > wconv; return wconv.from_bytes( utf8 ); } #endif // _WIN32 // encode url so it's valid even with UTF-8 characters string encodeUrl( const string &url ) { // stolen from here - // https://stackoverflow.com/questions/154536/encode-decode-urls-in-c #ifdef _WIN32 std::wstringstream encoded; auto url_c = wstring_to_utf8( url ); #else std::stringstream encoded; const auto &url_c = url; #endif encoded.fill( '0' ); encoded << std::hex; for ( const auto &x : url_c ) { if ( isalnum( static_cast< unsigned char >( x ) ) || x == '-' || x == '_' || x == '.' || x == '~' ) { encoded << x; continue; } encoded << std::uppercase << '%' << std::setw( 2 ); encoded << int( static_cast< unsigned char >( x ) ) << std::nouppercase; } return encoded.str(); } // return true if filename has specified season // set ep_pos to position where episode number starts bool searchSpecificSeason( const char_t *const path, size_t &ep_pos, const string &number ) { size_t cur_pos{}; #ifdef _WIN32 auto ncompare = wcsncmp; #else auto ncompare = strncmp; #endif // search for S[0-9]+E[0-9]+ while ( path[cur_pos] != '\0' ) { if ( ( path[cur_pos] == 's' || path[cur_pos] == 'S' ) && iswdigit( path[cur_pos + 1] ) ) { cur_pos++; while ( path[cur_pos] == '0' ) cur_pos++; // if season number is 0, move back because previous while skipped it if( number == TEXT("0") ) cur_pos--; // make sure season's number is the same as provided in argument // `number` if ( !ncompare( path + cur_pos, number.c_str(), number.size() ) ) { cur_pos += number.size(); if ( ( path[cur_pos] == 'e' || path[cur_pos] == 'E' ) && iswdigit( path[cur_pos + 1] ) ) { ep_pos = cur_pos + 1; return true; } } } cur_pos++; } return false; } bool searchSpecificSeason( const char_t *const p, const string &number ) { size_t tmp; return searchSpecificSeason( p, tmp, number ); } // return true if file contains S[0-9]+E[0-9]+ nad set // season_pos to start of season number bool searchSeason( const char_t *const path, size_t &season_pos ) { size_t cur_pos{}; while ( path[cur_pos] != '\0' ) { if ( ( path[cur_pos] == 's' || path[cur_pos] == 'S' ) && iswdigit( path[cur_pos + 1] ) ) { cur_pos++; season_pos = cur_pos; // after ++ because we want the first pos to // point to season's number while ( iswdigit( path[cur_pos] ) ) cur_pos++; if ( ( path[cur_pos] == 'e' || path[cur_pos] == 'E' ) && iswdigit( path[cur_pos + 1] ) ) { return true; } } cur_pos++; } return false; } bool searchSeason( const char_t *const path ) { size_t tmp{}; return searchSeason( path, tmp ); } void iterateFS( std::map< int, std::set< string > > &seasons, const string &path ) { // season_pos - position of first digit of the season size_t season_pos{ string::npos }; for ( const auto p : FSLib::Directory( path ) ) { // if p is directory, iterate through it if ( FSLib::isDirectory( path + dir_divider + p ) ) { iterateFS( seasons, path + dir_divider + p ); continue; } // if file is a correct format, add it to file list // for its season if ( searchSeason( p, season_pos ) ) seasons[std::stoi( p + season_pos )].insert( path + dir_divider + p ); } } #ifndef GUI // following functions are only needed for CLI version // find all files for provided season in `path` and store it in `files` void findSeason( std::set< string > &files, int season, const string &path ) { #ifdef _WIN32 auto number = std::to_wstring( season ); #else auto number = std::to_string( season ); #endif for ( const auto p : FSLib::Directory( path ) ) { // if p is directory, iterate through it if ( FSLib::isDirectory( path + dir_divider + p ) ) { findSeason( files, season, path + dir_divider + p ); continue; } if ( searchSpecificSeason( p, number ) ) files.insert( path + dir_divider + p ); } } // find all files that comply with the S[0-9]+E[0-9]+ pattern // and their season is in season_numbers and store tem in `seasons` void findSeasons( std::map< int, std::set< string > > &seasons, const string &path, const std::set< int > &season_numbers ) { // season_pos - position of first digit of the season size_t season_pos{ string::npos }; for ( const auto p : FSLib::Directory( path ) ) { // if p is directory, iterate through it if ( FSLib::isDirectory( path + dir_divider + p ) ) { findSeasons( seasons, path + dir_divider + p, season_numbers ); continue; } if ( searchSeason( p, season_pos ) ) { auto num = std::stoi( p + season_pos ); if ( season_numbers.find( num ) != season_numbers.end() ) seasons[num].insert( path + dir_divider + p ); } } } string getDefUrl( string &show, const string &language, Curl &c ) { string base_url = TEXT( "https://www.thetvdb.com" ); #ifdef _WIN32 string source_code = utf8_to_wstring( c.execute( TEXT( "https://www.thetvdb.com/search?q=" ) + encodeUrl( show ) + TEXT( "&l=" ) + language ) ); #else string source_code = c.execute( TEXT( "https://www.thetvdb.com/search?q=" ) + encodeUrl( show ) + TEXT( "&l=" ) + language ); #endif size_t order{}, pos{}; std::vector< std::pair< string, string > > urls; // find all possible shows while ( true ) { pos = source_code.find( TEXT( "/ser" ), pos ); if ( pos != string::npos ) { auto end = source_code.find( TEXT( ">" ), pos ); end--; auto end2 = source_code.find( TEXT( "<" ), end + 2 ); // store shows in urls, first is name, second is url urls.emplace_back( source_code.substr( end + 2, end2 - end - 2 ), source_code.substr( pos, end - pos ) ); cout << ++order << ". " << urls.back().first << std::endl; pos = end2; } else { break; } } cout << "Which TV Show is the right one? "; cin >> pos; cin.clear(); cin.ignore( 1, '\n' ); show = urls[pos - 1].first; return base_url + urls[pos - 1].second; } void printHelp() { cout << "usage: tv_rename [--help] [--show show name] [--season season " "number]" << std::endl; cout << " [--correct-path] [--show-path show path] [--trust]" << std::endl; cout << " [--linux] [--lang language] [--print-langs]" << std::endl; cout << std::endl << "Rename TV episodes" << std::endl << std::endl << "optional arguments:" << std::endl; cout << " -h, --help\t\tshow this help message and exit" << std::endl; cout << " --show show name, -s show name" << std::endl; cout << "\t\t\tTV show from which you want episode names (needs to be" << std::endl; cout << "\t\t\tin quotation marks if it has more than one word)" << std::endl; cout << " --season season number, -n season number" << std::endl; cout << "\t\t\tSeason number/s (if multiple seasons, put them in" << std::endl; cout << "\t\t\tquotation marks and seperate by one space)" << std::endl; cout << "\t\t\tor 'all' for all seasons in selected subdirectory" << std::endl; cout << " --show-path show path, -p show path" << std::endl; cout << "\t\t\tPath of the directory with episodes" << std::endl; cout << " --correct-path, -c\tThis is the correct path, stop asking me!" << std::endl; cout << " --name-pattern pattern" << std::endl; cout << "\t\t\tPattern to which change the file name. Possible sequences " "are:" << std::endl; cout << "\t\t\t\t%filename - original filename (without filetype extension)" << std::endl; cout << "\t\t\t\t%show - show name from thetvdb" << std::endl; cout << "\t\t\t\t%epname - episode name from thetvdb" << std::endl; cout << "\t\t\t\t%season - season number" << std::endl; cout << "\t\t\t\ttpossible to specify leading 0 like this: %2season " "(number means how many leading zeros)" << std::endl; cout << "\t\t\t\t%episode - episode number" << std::endl; cout << "\t\t\t\t\tpossible to specify leading 0 like this: %2episode " "(number means how many leading zeros)" << std::endl; cout << "\t\t\tDefault pattern is \"$filename - $epname\"" << std::endl; cout << " --trust, -t\t\tDon't ask whether the names are correct" << std::endl; cout << " --linux, -x\t\tDon't replace characters characters that are " "illegal in Windows" << std::endl; cout << " --lang language, -l language" << std::endl; cout << "\t\t\tSelect which language the episode names shoud be in" << std::endl; cout << " --print-langs\t\tPring available language" << std::endl; } // parse command line argument --seasons (e.g. '1 2 3 4 5') // and store season numbers as integers in seasons_num void parseSeasonNumbers( std::set< int > &seasons_num, const char_t *argument ) { size_t pos{ 0 }; while ( !iswdigit( argument[pos] ) && argument[pos] != '\0' ) pos++; if ( argument[pos] == '\0' ) { seasons_num.clear(); return; } int temp; #ifdef _WIN32 std::wstringstream iss( argument + pos ); #else std::stringstream iss( argument + pos ); #endif while ( iss >> temp ) { seasons_num.insert( temp ); } } // print possible language codes and their corresponding language void printLangs() { for ( size_t i = 0; i < languages.size(); i += 2 ) { cout << languages[i] << " - " << languages[i + 1] << std::endl; } } // make sure language is a valide language code bool findLanguage( const char_t *language ) { for ( size_t i = 0; i < languages.size(); i += 2 ) { #ifdef _WIN32 if ( !wcscmp( language, languages[i] ) ) #else if ( !strcmp( language, languages[i] ) ) #endif return true; } return false; } #else // GUI // functions that are needed for GUI but not for CLI // get possible shows for search query in `show` std::vector< std::pair< string, string > > getPossibleShows( string show, const string &language, Curl &c ) { // encode show name so it can be resolved as url show = encodeUrl( show ); #ifdef _WIN32 auto source_code = utf8_to_wstring( c.execute( TEXT( "https://www.thetvdb.com/search?q=" ) + show + TEXT( "&l=" ) + language ) ); #else auto source_code = c.execute( TEXT( "https://www.thetvdb.com/search?q=" ) + show + TEXT( "&l=" ) + language ); #endif size_t pos{}; std::vector< std::pair< string, string > > urls; while ( true ) { pos = source_code.find( TEXT( "/ser" ), pos ); if ( pos != string::npos ) { auto end = source_code.find( TEXT( ">" ), pos ); auto end2 = source_code.find( TEXT( "<" ), end + 1 ); end--; urls.emplace_back( source_code.substr( end + 2, end2 - ( end + 2 ) ), source_code.substr( pos, end - pos ) ); pos = end + 2; } else { break; } } return urls; } #ifndef _WIN32 // get user's home directory string userHome() { uid_t user_uid; // current user's uid { uid_t eid; uid_t sid; getresuid( &user_uid, &eid, &sid ); // don't need eid and sid } // password file entry auto user_passwd = getpwuid( user_uid ); if ( user_passwd == nullptr ) throw std::runtime_error( "User with uid " + std::to_string( user_uid ) + " doesn't exist!" ); return user_passwd->pw_dir; } #else // UNIX // get user's %APPDATA% folder location string userHome() { wchar_t *dir = static_cast< wchar_t * >( CoTaskMemAlloc( MAX_PATH ) ); auto res = SHGetKnownFolderPath( FOLDERID_RoamingAppData, 0, NULL, &dir ); if ( res == S_OK ) { string dir_s = dir; CoTaskMemFree( dir ); return dir_s; } return L""; } #endif // UNIX #endif // ndef GUI // create file name based on given pattern string compilePattern( const string &pattern, int season, int episode, const string &filename, const string &episodeName, const string &showName ) { string output; #ifdef _WIN32 auto season_num = std::to_wstring( season ); auto ep_num = std::to_wstring( episode ); #else auto season_num = std::to_string( season ); auto ep_num = std::to_string( episode ); #endif for ( size_t i = 0; i < pattern.size(); i++ ) { // if current character is % check if a pattern follows, otherwise // put % if ( pattern[i] == '%' ) { // check for numbers right after % indicating size of zero // padding for numbers auto pos = pattern.find_first_not_of( TEXT( "0123456789" ), i + 1 ); if ( pattern.find( TEXT( "season" ), pos - 1 ) == pos && pos != i + 1 ) { // if season is AFTER numbers, put season number padded // with zeros // get number of leading zeros auto leading = std::stoi( pattern.c_str() + i + 1 ); // move i to the last char of 'season' i = pos + 5; // get number of zeros to be put before the season number leading -= season_num.size(); if ( leading < 0 ) leading = 0; // add padded season to output output += string( leading, '0' ) + season_num; } else if ( pattern.find( TEXT( "season" ), i ) == i + 1 ) { // if season isn't after numbers, just put season number to // output i += 6; output += season_num; } else if ( pattern.find( TEXT( "episode" ), pos - 1 ) == pos && pos != i + 1 ) { // same principle as with season after number auto leading = std::stoi( pattern.c_str() + i + 1 ); i = pos + 6; leading -= ep_num.size(); if ( leading < 0 ) leading = 0; output += string( leading, '0' ) + ep_num; } else if ( pattern.find( TEXT( "episode" ), i ) == i + 1 ) { // if episode isn't after number, just put the episode number to // output i += 7; output += ep_num; } else if ( pattern.find( TEXT( "epname" ), i ) == i + 1 ) { // episode name from thetvdb i += 6; output += episodeName; } else if ( pattern.find( TEXT( "show" ), i ) == i + 1 ) { // show name from thetvdb i += 4; output += showName; } else if ( pattern.find( TEXT( "filename" ), i ) == i + 1 ) { // original file name i += 8; output += filename; } else { // output % if no escape sequence was found output += '%'; } } else if ( pattern[i] == '\\' ) { // possibility to escape % if ( pattern[i + 1] == '%' ) { output += '%'; i++; } else if ( pattern[i + 1] == '\\' ) { output += '\\'; i++; } else { output += '\\'; } } else { // if char isn't % or / just add it to the output string output += pattern[i]; } } return output; }