commit 9413214fce067747d6564ef83ec9848737b0bf00 Author: zv0n Date: Thu Jul 8 07:41:34 2021 +0200 Initial diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..e83ad73 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,43 @@ +cmake_minimum_required(VERSION 3.10) +set(CMAKE_CXX_STANDARD 14) +set(CMAKE_CXX_STANDARD_REQUIRED True) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +include_directories(/usr/local/include) +link_directories(/usr/local/lib) + + +project(RenameServer) + +add_executable(RenameServer + main.cpp + rename_object.cpp + functions.cpp + config/config.cpp + filesystem/unix/filesystem.cpp + jwt.cpp + ) +target_link_libraries(RenameServer restbed config++ jwt) + +add_library(thetvdb SHARED + thetvdb/tv_rename.cpp + thetvdb/functions.cpp + rename_object.cpp + filesystem/unix/filesystem.cpp + network/unix/network.cpp) + +target_link_libraries(thetvdb curl) + +add_library(simple SHARED + simple_rename/simple.cpp + rename_object.cpp + filesystem/unix/filesystem.cpp) + +add_library(moviedb SHARED + themoviedb/moviedb.cpp + themoviedb/functions.cpp + rename_object.cpp + filesystem/unix/filesystem.cpp + network/unix/network.cpp) + +target_link_libraries(moviedb curl) diff --git a/config/config.cpp b/config/config.cpp new file mode 100644 index 0000000..7ac7e44 --- /dev/null +++ b/config/config.cpp @@ -0,0 +1,50 @@ +#include "config.hpp" +#include +#include + +bool Configuration::readConfiguration(const std::string &file) { + libconfig::Config cfg; + try { + cfg.readFile(file); + } catch (const libconfig::FileIOException &fioex) { + std::cerr << "Couldn't open configuration file" << std::endl; + return false; + } catch (const libconfig::ParseException &pex) { + std::cerr << "Couldn't parse configuration file" << std::endl; + return false; + } + cfg.lookupValue("source_path", source_path); + auto &targets = cfg.getRoot()["target_paths"]; + for(auto &target : targets) { + target_paths.emplace_back(target["path"], target["name"]); + } + auto &cfg_libraries = cfg.getRoot()["libraries"]; + for(auto &library : cfg_libraries) { + libraries.emplace_back( + library.lookup("path"), + library.lookup("name"), + library.lookup("config")); + } + + auto &cfg_users = cfg.getRoot()["users"]; + for(auto &user : cfg_users) { + users.emplace_back( + user.lookup("user"), + user.lookup("password")); + } + + return true; +} + +const std::string &Configuration::getSourcePath() { + return source_path; +} +const std::vector> &Configuration::getLibraries() { + return libraries; +} +const std::vector> &Configuration::getTargetPaths() { + return target_paths; +} +const std::vector> &Configuration::getUsers() { + return users; +} diff --git a/config/config.hpp b/config/config.hpp new file mode 100644 index 0000000..f3b8b17 --- /dev/null +++ b/config/config.hpp @@ -0,0 +1,21 @@ +#ifndef RS_CONFIG_HPP +#define RS_CONFIG_HPP + +#include +#include + +class Configuration { +public: + bool readConfiguration(const std::string &file); + const std::string &getSourcePath(); + const std::vector> &getLibraries(); + const std::vector> &getTargetPaths(); + const std::vector> &getUsers(); +private: + std::vector> libraries; + std::string source_path; + std::vector> target_paths; + std::vector> users; +}; + +#endif diff --git a/filesystem/filesystem.hpp b/filesystem/filesystem.hpp new file mode 100644 index 0000000..90c2ea7 --- /dev/null +++ b/filesystem/filesystem.hpp @@ -0,0 +1,126 @@ +#ifndef FSLIB_H +#define FSLIB_H + +#include + +#ifdef _WIN32 + +#include + +using string = std::wstring; +using char_t = wchar_t; + +#else + +#include + +using string = std::string; +using char_t = char; + +#endif + +// windows version stolen from +// http://www.martinbroadhurst.com/list-the-files-in-a-directory-in-c.html + +namespace FSLib { + +bool exists( const string &path ); +bool isDirectory( const string &path ); +bool rename( const string &file_a, const string &file_b ); +string canonical( const string &path ); +bool createDirectoryFull( const string &path ); +string getContainingDirectory( const string &path ); +string getFileName( const string &path ); +string getFileExtension( const string &path ); +extern char dir_divisor; + +class Directory { +public: + Directory() = delete; + explicit Directory( const string &path_ ); + explicit Directory( const Directory &d ) = default; + explicit Directory( Directory &&d ) = default; + + class Iterator { + public: + explicit Iterator( const Directory &d_ ); + ~Iterator(); + +#ifdef _WIN32 + explicit Iterator( bool ended_ ); +#else + Iterator( const Directory &d_, const struct dirent *current_entry_ ); +#endif + + Iterator() = delete; + + Iterator( const Iterator &i ) = default; + + Iterator( Iterator &&i ) = default; + + char_t const *operator*() const; + + Iterator &operator++(); + + bool operator==( const Iterator &i_other ) const; + + Iterator operator++( int ) { + Iterator ret( *this ); + operator++(); + return ret; + } + + bool operator!=( const Iterator &i_other ) const { + return !( i_other == *this ); + } + + private: +#ifndef _WIN32 + DIR *d{}; + const struct dirent *current_entry{}; +#else + HANDLE hFind{}; + WIN32_FIND_DATA data{}; + bool ended{ false }; +#endif + }; + + using iterator = Iterator; + using const_iterator = Iterator; + + iterator end(); + + const_iterator end() const; + + iterator begin() { + return Iterator( *this ); + } + + const_iterator begin() const { + return Iterator( *this ); + } + + const_iterator cbegin() const { + return begin(); + } + + const_iterator cend() const { + return end(); + } + + const char_t *path() const { + return dir_path.c_str(); + } + +#ifdef _WIN32 + const char_t *validPath() const { + return dir_path.substr( 0, dir_path.length() - 2 ).c_str(); + } +#endif + +private: + string dir_path; +}; +} // namespace FSLib + +#endif diff --git a/filesystem/unix/filesystem.cpp b/filesystem/unix/filesystem.cpp new file mode 100644 index 0000000..16c4792 --- /dev/null +++ b/filesystem/unix/filesystem.cpp @@ -0,0 +1,145 @@ +#include "../filesystem.hpp" +#include +#include +#ifdef __APPLE__ +#include +#endif +#include +#include + +char FSLib::dir_divisor = '/'; + +FSLib::Directory::Directory( const string &path_ ) : dir_path( path_ ) {} + +FSLib::Directory::Iterator::Iterator( const Directory &d_ ) + : d( opendir( d_.path() ) ) { + if ( !exists( d_.path() ) || !isDirectory( d_.path() ) ) { + throw std::runtime_error( + std::string( "Directory " ) + d_.path() + + " either doesn't exist or isn't a directory" ); + } + + current_entry = readdir( d ); + + // skip "." and ".." + if ( current_entry != nullptr && + ( !strcmp( current_entry->d_name, "." ) || + !strcmp( current_entry->d_name, ".." ) ) ) + ++( *this ); +} + +FSLib::Directory::Iterator::Iterator( const Directory &d_, + const struct dirent *current_entry_ ) + : d( opendir( d_.path() ) ), current_entry( current_entry_ ) {} + +FSLib::Directory::Iterator::~Iterator() { + if ( d ) + closedir( d ); +} + +bool FSLib::exists( const string &path ) { + struct stat path_stat; + return stat( path.c_str(), &path_stat ) == 0; +} + +string FSLib::canonical( const string &path ) { + char_t *canonical_path = new char_t[PATH_MAX]; + auto failed = realpath( path.c_str(), canonical_path ) == nullptr; + + if ( failed ) { + delete[] canonical_path; + return string(); + } + + string canonical_string{ canonical_path }; + delete[] canonical_path; + return canonical_string; +} + +bool FSLib::isDirectory( const string &path ) { + struct stat path_stat; + + if ( stat( path.c_str(), &path_stat ) != 0 ) + return false; + + return S_ISDIR( path_stat.st_mode ); +} + +bool FSLib::rename( const string &file_a, const string &file_b ) { + // TODO log + std::cout << file_a << " -> " << file_b << std::endl; + return ::rename( file_a.c_str(), file_b.c_str() ) == 0; +} + +bool FSLib::createDirectoryFull( const string &path ) { + uint64_t pos{}; + // go through all directories leading to the last one + // and create them if they don't exist + do { + // get partial directory path + pos = path.find_first_of( "/", pos ); + if ( pos > 0 ) { + auto dirname = path.substr( 0, pos ); + // create it if it doesn't exist + if ( !FSLib::exists( dirname ) ) { + if ( mkdir( dirname.c_str(), + S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH ) != 0 ) + return false; + } + } + pos++; + } while ( pos < path.length() && pos != 0 ); + return true; +} + +FSLib::Directory::iterator FSLib::Directory::end() { + return Iterator( *this, nullptr ); +} + +FSLib::Directory::const_iterator FSLib::Directory::end() const { + return Iterator( *this, nullptr ); +} + +char_t const *FSLib::Directory::Iterator::operator*() const { + return current_entry->d_name; +} + +FSLib::Directory::Iterator &FSLib::Directory::Iterator::operator++() { + if ( current_entry == nullptr ) + return *this; + current_entry = readdir( d ); + // skip . and .. + if ( current_entry != nullptr && + ( !strcmp( current_entry->d_name, "." ) || + !strcmp( current_entry->d_name, ".." ) ) ) + return operator++(); + return *this; +} + +bool FSLib::Directory::Iterator::operator==( const Iterator &i_other ) const { + return i_other.current_entry == current_entry; +} + +string FSLib::getContainingDirectory( const string &path ) { + auto pos = path.find_last_of('/'); + if(pos == string::npos) { + return "."; + } + return path.substr(0, pos); +} + +string FSLib::getFileName( const string &path ) { + auto pos = path.find_last_of('/'); + if(pos == string::npos) { + return path; + } + return path.substr(pos+1); +} + +string FSLib::getFileExtension( const string &path ) { + auto pos = path.find_last_of('.'); + if(pos == string::npos) { + return ""; + } + return path.substr(pos + 1); +} diff --git a/filesystem/windows/filesystem.cpp b/filesystem/windows/filesystem.cpp new file mode 100644 index 0000000..70739f4 --- /dev/null +++ b/filesystem/windows/filesystem.cpp @@ -0,0 +1,156 @@ +#include "../filesystem.hpp" +#include +#include +#include +#include + +char FSLib::dir_divisor = '\\'; + +FSLib::Directory::Directory( const string &path_ ) : dir_path( path_ ) { + // need to append \\* for windows to search files in directory + dir_path.append( L"\\*" ); +} + +FSLib::Directory::Iterator::Iterator( const Directory &d_ ) { + if ( !exists( d_.validPath() ) || !isDirectory( d_.validPath() ) ) { + throw std::runtime_error( + "Directory either doesn't exist or isn't a directory" ); + } + hFind = FindFirstFileW( d_.path(), &data ); + if ( hFind != INVALID_HANDLE_VALUE ) { + if ( !wcscmp( data.cFileName, L"." ) || + !wcscmp( data.cFileName, L".." ) ) { + ++( *this ); + } + } else { + ended = true; + } +} + +FSLib::Directory::Iterator::~Iterator() { + if ( hFind ) + FindClose( hFind ); +} + +// this is definitely not a good way to create the "end" iterator +// but it was the only way I thought of with my limited knowledge of +// windows.h +FSLib::Directory::Iterator::Iterator( bool ended_ ) : ended( ended_ ) {} + +bool FSLib::exists( const string &path ) { + struct _stat path_stat; + return _wstat( path.c_str(), &path_stat ) == 0; +} + +string FSLib::canonical( const string &path ) { + char_t *full_path = new char_t[MAX_PATH]; + char_t *canonical_path = new char_t[MAX_PATH]; + + auto failed = !GetFullPathName( path.c_str(), MAX_PATH, full_path, NULL ); + + if ( failed ) { + delete[] canonical_path; + delete[] full_path; + return string(); + } + + failed = !PathCanonicalizeW( canonical_path, full_path ); + + delete[] full_path; + + if ( failed ) { + delete[] canonical_path; + return string(); + } + + string canonical_string{ canonical_path }; + delete[] canonical_path; + return canonical_string; +} + +bool FSLib::isDirectory( const string &path ) { + struct _stat path_stat; + + if ( _wstat( path.c_str(), &path_stat ) != 0 ) + return false; + + return path_stat.st_mode & _S_IFDIR; +} + +bool FSLib::rename( const string &file_a, const string &file_b ) { + return MoveFileExW( file_a.c_str(), file_b.c_str(), + MOVEFILE_COPY_ALLOWED | MOVEFILE_REPLACE_EXISTING ); +} + +bool FSLib::createDirectoryFull( const string &path ) { + uint64_t pos = path.find_first_of( L":", 0 ) + 2; + if ( pos == string::npos ) + pos = 0; + // go through all directories leading to the last one + // and create them if they don't exist + do { + // get partial directory path + pos = path.find_first_of( L"\\", pos ); + auto dirname = path.substr( 0, pos ); + // create it if it doesn't exist + if ( !FSLib::exists( dirname ) ) { + if ( !CreateDirectoryW( dirname.c_str(), NULL ) ) + return false; + } + pos++; + } while ( pos < path.length() && pos != 0 ); + return true; +} + +FSLib::Directory::iterator FSLib::Directory::end() { + return Iterator( true ); +} + +FSLib::Directory::const_iterator FSLib::Directory::end() const { + return Iterator( true ); +} + +char_t const *FSLib::Directory::Iterator::operator*() const { + return data.cFileName; +} + +FSLib::Directory::Iterator &FSLib::Directory::Iterator::operator++() { + if ( ended == true ) + return *this; + // skip . and .. + if ( FindNextFileW( hFind, &data ) == 0 ) { + ended = true; + } else if ( !wcscmp( data.cFileName, L"." ) || + !wcscmp( data.cFileName, L".." ) ) { + return operator++(); + } + return *this; +} + +bool FSLib::Directory::Iterator::operator==( const Iterator &i_other ) const { + return i_other.ended == ended; +} + +string FSLib::getContainingDirectory( const string &path ) { + auto pos = path.find_last_of('\\'); + if(pos == string::npos) { + return "."; + } + return path.substr(0, pos); +} + +string FSLib::getFileName( const string &path ) { + auto pos = path.find_last_of('\\'); + if(pos == string::npos) { + return path; + } + return path.substr(pos+1); +} + +string FSLib::getFileExtension( const string &path ) { + auto pos = path.find_last_of('.'); + if(pos == string::npos) { + return ""; + } + return path.substr(pos + 1); +} diff --git a/functions.cpp b/functions.cpp new file mode 100644 index 0000000..a90dfc0 --- /dev/null +++ b/functions.cpp @@ -0,0 +1,89 @@ +#include +#include +#include "functions.hpp" +#include "filesystem/filesystem.hpp" + +std::vector< RenameLibrary > getLibraries(const std::vector> &libraries) { + // TODO get from config file + std::vector< RenameLibrary > result{}; + for ( auto &library : libraries ) { + void *libhndl = dlopen( std::get<0>(library).c_str(), RTLD_NOW ); + if ( !libhndl ) { + std::cerr << "Could not load library " << std::get<0>(library) << std::endl; + closeLibraries( result ); + return {}; + } + RenameLibrary rl; + rl.libhndl = libhndl; + rl.init = ( bool ( * )(const std::string &) )dlsym( libhndl, "init" ); + if ( !rl.init ) { + result.push_back( rl ); + goto dlsymerror; + } + rl.getOptions = ( std::vector< RenameObject >( * )( + const RenameObject & ) )dlsym( libhndl, "getOptions" ); + if ( !rl.getOptions ) { + result.push_back( rl ); + goto dlsymerror; + } + rl.renamePath = + ( bool ( * )( const std::string &, const RenameObject & ) )dlsym( + libhndl, "renamePath" ); + if ( !rl.renamePath ) { + result.push_back( rl ); + goto dlsymerror; + } + rl.getCustomKeys = ( std::vector< std::string >( * )() )dlsym( + libhndl, "getCustomKeys" ); + if ( !rl.getCustomKeys ) { + result.push_back( rl ); + goto dlsymerror; + } + rl.name = std::get<1>(library); + rl.config = std::get<2>(library); + result.push_back( rl ); + } + return result; + +dlsymerror: + std::cerr << "dlsym: " << dlerror() << std::endl; + closeLibraries( result ); + return {}; +} + +void closeLibraries( std::vector< RenameLibrary > &libraries ) { + for ( auto &library : libraries ) { + dlclose( library.libhndl ); + } +} + +void addFilesRecursive(const std::string &prefix, std::vector< std::string > &results, const std::string &filename, const std::string &containing_directory, bool dir_only = false ) { + auto path = containing_directory + FSLib::dir_divisor + filename; + if(!dir_only || FSLib::isDirectory(path)) { + results.push_back(prefix + filename); + } + if( FSLib::isDirectory(path) ) { + auto newprefix = prefix + filename + FSLib::dir_divisor; + for(const auto &entry : FSLib::Directory(path)) { + addFilesRecursive(newprefix, results, entry, path, dir_only); + } + } +} + +std::vector< std::string > getFilesInSource( const std::string &source_dir ) { + std::vector< std::string > result; + for(const auto &entry : FSLib::Directory(source_dir)) { + addFilesRecursive("", result, entry, source_dir); + } + std::sort(result.begin(), result.end()); + return result; +} + +std::vector< std::string > getTargetDirectories( const std::string &target_dir ) { + std::vector< std::string > result; + for(const auto &entry : FSLib::Directory(target_dir)) { + addFilesRecursive("", result, entry, target_dir, true); + } + std::sort(result.begin(), result.end()); + return result; +} diff --git a/functions.hpp b/functions.hpp new file mode 100644 index 0000000..d669a87 --- /dev/null +++ b/functions.hpp @@ -0,0 +1,12 @@ +#ifndef RENAME_FUNCTIONS_H +#define RENAME_FUNCTIONS_H + +#include +#include "rename_library.hpp" + +std::vector< RenameLibrary > getLibraries(const std::vector> &libraries); +void closeLibraries( std::vector< RenameLibrary > &libraries ); +std::vector< std::string > getFilesInSource( const std::string &source_dir ); +std::vector< std::string > getTargetDirectories( const std::string &target_dir ); + +#endif diff --git a/jwt.cpp b/jwt.cpp new file mode 100644 index 0000000..8622a7a --- /dev/null +++ b/jwt.cpp @@ -0,0 +1,64 @@ +#include "jwt.hpp" +#include +#include + +const std::string secret_key = "supersecretkey"; + +std::string createLoginToken(const std::string &user) { + auto alg = JWT_ALG_HS256; + time_t iat = time(NULL); + time_t exp = iat + 60*60; + std::string result = ""; + + jwt_t *jwt; + auto ret = jwt_new(&jwt); + if (ret != 0 || jwt == NULL) { + fprintf(stderr, "invalid jwt\n"); + goto finish; + } + jwt_add_grant_int(jwt, "iat", iat); + jwt_add_grant_int(jwt, "exp", exp); + jwt_add_grant(jwt, "user", user.c_str()); + jwt_add_grant_bool(jwt, "valid", true); + jwt_set_alg(jwt, alg, reinterpret_cast(secret_key.c_str()), secret_key.length()); + result = std::string(jwt_encode_str(jwt)); +finish: + jwt_free(jwt); + return result; +} + +bool verifyJWT(const std::string &token) { + jwt_valid_t *jwt_valid; + jwt_t *jwt; + bool result = false; + auto ret = jwt_valid_new(&jwt_valid, JWT_ALG_HS256); + if (ret != 0 || jwt_valid == NULL) { + fprintf(stderr, "failed to allocate jwt_valid\n"); + goto finish_valid; + } + jwt_valid_set_headers(jwt_valid, 1); + jwt_valid_set_now(jwt_valid, time(NULL)); + jwt_valid_add_grant_bool(jwt_valid, "valid", true); + + ret = jwt_decode(&jwt, token.c_str(), reinterpret_cast(secret_key.c_str()), secret_key.length()); + if (ret != 0 || jwt == NULL) { + fprintf(stderr, "invalid jwt\n"); + goto finish; + } + if (jwt_validate(jwt, jwt_valid) != 0) { + fprintf(stderr, "jwt failed to validate: %08x\n", jwt_valid_get_status(jwt_valid)); + jwt_dump_fp(jwt, stderr, 1); + goto finish; + } + + result = true; + std::cout << "USER " << jwt_get_grant(jwt, "user") << " LOGGED IN!" << std::endl; + +finished_valid: + finish: + jwt_free(jwt); +finish_valid: + jwt_valid_free(jwt_valid); + + return result; +} diff --git a/jwt.hpp b/jwt.hpp new file mode 100644 index 0000000..1c9a3ee --- /dev/null +++ b/jwt.hpp @@ -0,0 +1,9 @@ +#ifndef JWT_HPP +#define JWT_HPP + +#include + +std::string createLoginToken(const std::string &user); +bool verifyJWT(const std::string &token); + +#endif diff --git a/library.hpp b/library.hpp new file mode 100644 index 0000000..32933b7 --- /dev/null +++ b/library.hpp @@ -0,0 +1,25 @@ +#ifndef SIMPLE_RENAME_HPP +#define SIMPLE_RENAME_HPP + +#include + +#include "rename_object.hpp" + +#ifdef _WIN32 + +using string = std::wstring; + +#else + +using string = std::string; + +#endif + +extern "C" { +bool init(const string &configuration); +std::vector< RenameObject > getOptions( const RenameObject &search ); +bool renamePath( const string &path, const RenameObject &renamer ); +std::vector< string > getCustomKeys(); +} + +#endif // TV_RENAME_HPP diff --git a/main.cpp b/main.cpp new file mode 100644 index 0000000..99de751 --- /dev/null +++ b/main.cpp @@ -0,0 +1,501 @@ +#include "rename_object.hpp" +#include "functions.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "jwt.hpp" + +#include "filesystem/filesystem.hpp" + +#include "config/config.hpp" + +std::vector libraries{}; +Configuration cfg; + +void sendResponse(const std::string &response, int status_code, const std::shared_ptr< restbed::Session > session) { + session->close(status_code, response, { { "Content-Length", std::to_string(response.length()) }, { "Access-Control-Allow-Origin", "*" } }); +} + +void performPostFunc(const std::shared_ptr session, std::function, rapidjson::GenericDocument>&)> callback) { + const auto request = session->get_request( ); + + int content_length = request->get_header( "Content-Length", 0 ); + + session->fetch( content_length, [callback]( const std::shared_ptr< restbed::Session > session, const restbed::Bytes & body ) + { + rapidjson::Document doc; + std::cout << std::string(reinterpret_cast(body.data()), body.size()) << std::endl; + doc.Parse(reinterpret_cast(body.data()), body.size()); + if(doc.HasParseError()) { + sendResponse("ERROR: Invalid body!", 401, session); + return; + } + callback(session, doc); + }); +} + +bool verifyLogin( const std::shared_ptr< restbed::Session > &session, rapidjson::GenericDocument> &doc ) { + std::cout << "WAAA" << std::endl; + if(doc.FindMember("token") == doc.MemberEnd() || !doc["token"].IsString()) { + sendResponse("ERROR: Invalid token!", 401, session); + return false; + } + std::cout << "AAAA" << std::endl; + auto token = doc["token"].GetString(); + auto res = verifyJWT(token); + if(!res) { + sendResponse("ERROR: Invalid token!", 401, session); + } + return res; +} + +std::vector> getTypes() { + std::vector> result{}; + for(size_t i = 0; i < libraries.size(); i++) { + result.emplace_back(libraries[i].name, i); + } + return result; +} + +std::string getTypesJson() { + auto types = getTypes(); + std::ostringstream result; + result << "[\n"; + if(types.size() > 0) { + for(auto type : types) { + result << " {\n \"id\": " << type.second << "\n"; + result << " \"name\": \"" << type.first << "\"\n },\n"; + } + result.seekp(-2, std::ios_base::end); + result << "\n"; + } + result << "]"; + return result.str(); +} + +void getTypesRest( const std::shared_ptr< restbed::Session > session ) { + sendResponse(getTypesJson(), restbed::OK, session); +} + +std::vector< RenameObject > getOptions(const RenameObject &search, size_t type) { + if(type >= libraries.size()) { + return {}; + } + auto result = libraries[type].getOptions(search); + for(auto &res : result) { + res.setLibraryId(type); + } + return result; +} + +std::string getOptionsJson(const RenameObject &search, size_t type) { + std::ostringstream res; + res << "[\n"; + auto options = getOptions(search, type); + if(options.size() > 0) { + for(auto &option : options) { + res << option.toJson(); + res << ",\n"; + } + res.seekp( -2, std::ios_base::end ); + res << "\n"; + } + res << "]"; + return res.str(); +} + +void getOptionsRest( const std::shared_ptr< restbed::Session > session, rapidjson::GenericDocument> &doc ) +{ + if(doc.HasParseError()) { + sendResponse("ERROR: Invalid body!", 401, session); + return; + } + if(!verifyLogin(session, doc)) { + return; + } + if(doc.FindMember("type") == doc.MemberEnd() || !doc["type"].IsUint64()) { + sendResponse("ERROR: Invalid type!", 401, session); + return; + } + if(doc.FindMember("search") == doc.MemberEnd() || !doc["search"].IsObject()) { + sendResponse("ERROR: Invalid search!", 401, session); + return; + } + size_t type_id = doc["type"].GetUint64(); + RenameObject search; + rapidjson::StringBuffer sb; + rapidjson::Writer writer(sb); + doc["search"].Accept(writer); + std::string s = sb.GetString(); + search.fromJson(s); + if(search.getPresentedName().empty()) { + sendResponse("Empty search", 401, session); + return; + } + sendResponse(getOptionsJson(search, type_id), 200, session); +} + +std::vector< std::string > getCustomKeys(size_t type) { + if(type >= libraries.size()) { + return {}; + } + auto result = libraries[type].getCustomKeys(); + return result; +} + +std::string getCustomKeysJson(size_t type) { + std::ostringstream res; + res << "[\n"; + auto custom_keys = getCustomKeys(type); + if(custom_keys.size() > 0) { + for(auto &key : custom_keys) { + res << "\"" << key << "\",\n"; + } + res.seekp( -2, std::ios_base::end ); + res << "\n"; + } + res << "]"; + return res.str(); +} + +void getCustomKeysRest( const std::shared_ptr< restbed::Session > session, rapidjson::GenericDocument> &doc ) +{ + if(doc.FindMember("type") == doc.MemberEnd() || !doc["type"].IsUint64()) { + sendResponse("ERROR: Invalid type!", 401, session); + return; + } + size_t type_id = doc["type"].GetUint64(); + sendResponse(getCustomKeysJson(type_id), 200, session); +} + +std::pair renamePath(std::string path, const RenameObject &renamer) { + if(renamer.getLibraryId() >= libraries.size()) { + return {false, "Invalid library id"}; + } + if(path[0] == '/' && path.substr(0, cfg.getSourcePath().length()) != cfg.getSourcePath()) { + return {false, "Invalid path"}; + } else if(path.find("..") != std::string::npos) { + return {false, "Path cannot contain '..'"}; + } else if(path[0] != '/') { + path = cfg.getSourcePath() + "/" + path; + } + if(!FSLib::exists(path)) { + return {false, "Source doesn't exist"}; + } + return {libraries[renamer.getLibraryId()].renamePath(path, renamer), "Library error"}; +} + +std::string renamePathJson(const std::string &path, const RenameObject &renamer) { + std::ostringstream res; + res << "{\n \"success\": "; + auto rename_result = renamePath(path, renamer); + if(rename_result.first) { + res << "true"; + } else { + res << "false"; + } + res << "\"\n"; + if(!rename_result.first) { + res << " \"error\": \"" << rename_result.second << "\"\n"; + } + res << "}"; + return res.str(); +} + +void renamePathRest( const std::shared_ptr< restbed::Session > session, rapidjson::GenericDocument> &doc ) { + if(!verifyLogin(session, doc)) { + return; + } + if(doc.FindMember("path") == doc.MemberEnd() || !doc["path"].IsString()) { + // TODO validate path, also validate against config + sendResponse("ERROR: Invalid path!", 401, session); + return; + } + if(doc.FindMember("renamer") == doc.MemberEnd() || !doc["renamer"].IsObject()) { + sendResponse("ERROR: Invalid renamer!", 401, session); + return; + } + std::string path = doc["path"].GetString(); + RenameObject renamer; + rapidjson::StringBuffer sb; + rapidjson::Writer writer(sb); + doc["renamer"].Accept(writer); + std::string s = sb.GetString(); + renamer.fromJson(s); + sendResponse(renamePathJson(path, renamer), 200, session); +} + +std::string getFilesJson() { + std::ostringstream res; + res << "[\n"; + auto files = getFilesInSource(cfg.getSourcePath()); + if(files.size() > 0) { + for(const auto &file : files) { + res << "\"" << file << "\",\n"; + } + res.seekp(-2, std::ios_base::end); + } + res << "\n]"; + return res.str(); +} + +void getFilesRest( const std::shared_ptr< restbed::Session > session, rapidjson::GenericDocument> &doc ) +{ + if(!verifyLogin(session, doc)) { + return; + } + sendResponse(getFilesJson(), restbed::OK, session); +} + +std::vector> getTargets() { + std::vector> result; + const auto &targets = cfg.getTargetPaths(); + for(uint64_t i = 0; i < targets.size(); i++) { + result.emplace_back(i, targets[i].second); + } + return result; +} + +std::string getTargetsJson() { + std::ostringstream res; + res << "[\n"; + auto targets = getTargets(); + if(targets.size() > 0) { + for(const auto &target : targets) { + res << " {\n" << " \"id\": " << target.first << "\n"; + res << " \"name\": \"" << target.second << "\"\n },\n"; + } + res.seekp(-2, std::ios_base::end); + } + res << "\n]"; + return res.str(); +} + +void getTargetsRest( const std::shared_ptr< restbed::Session > session, rapidjson::GenericDocument> &doc ) +{ + if(!verifyLogin(session, doc)) { + return; + } + sendResponse(getTargetsJson(), restbed::OK, session); +} + +std::string getTargetDirectoriesJson(uint64_t id) { + if(id >= cfg.getTargetPaths().size()) { + return ""; + } + std::ostringstream res; + res << "[\n"; + auto dirs = getTargetDirectories(cfg.getTargetPaths()[id].first); + if(dirs.size() > 0) { + for(const auto &dir : dirs) { + res << " \"" << dir << "\",\n"; + } + res.seekp(-2, std::ios_base::end); + } + res << "\n]"; + return res.str(); +} + +void getTargetDirectoriesRest( const std::shared_ptr< restbed::Session > session, rapidjson::GenericDocument> &doc ) +{ + if(!verifyLogin(session, doc)) { + return; + } + uint64_t id = 0; + if(doc.FindMember("path_id") == doc.MemberEnd() || !doc["path_id"].IsUint64() || (id = doc["path_id"].GetUint64()) >= cfg.getTargetPaths().size()) { + sendResponse("ERROR: Invalid path_id!", 401, session); + return; + } + sendResponse(getTargetDirectoriesJson(id), restbed::OK, session); +} + +std::pair move(std::string path, uint64_t target_id, const std::string &containing_dir) { + if(path[0] == '/' && path.substr(0, cfg.getSourcePath().length()) != cfg.getSourcePath()) { + return {false, "Invalid path"}; + } else if(path.find("..") != std::string::npos) { + return {false, "Path cannot contain '..'"}; + } else if(path[0] != '/') { + path = cfg.getSourcePath() + "/" + path; + } + if(target_id >= cfg.getTargetPaths().size()) { + return {false, "Invalid target_id"}; + } + if(!FSLib::exists(path)) { + return {false, "Source doesn't exist"}; + } + auto target_dir = cfg.getTargetPaths()[target_id].first + FSLib::dir_divisor + containing_dir; + if(!FSLib::exists(target_dir)) { + FSLib::createDirectoryFull(target_dir); + } + return {FSLib::rename(path, target_dir + FSLib::dir_divisor + FSLib::getFileName(path)), "Library error"}; +} + +std::string moveJson(const std::string &path, uint64_t target_id, const std::string &containing_dir) { + std::ostringstream res; + res << "{\n \"success\": "; + auto move_result = move(path, target_id, containing_dir); + if(move_result.first) { + res << "true"; + } else { + res << "false"; + } + res << "\"\n"; + if(!move_result.first) { + res << " \"error\": \"" << move_result.second << "\"\n"; + } + res << "}"; + return res.str(); +} + +void moveRest( const std::shared_ptr< restbed::Session > session, rapidjson::GenericDocument> &doc ) +{ + if(!verifyLogin(session, doc)) { + return; + } + std::string containing_dir = ""; + if(doc.FindMember("path") == doc.MemberEnd() || !doc["path"].IsString()) { + // TODO validate path, also validate against config + sendResponse("ERROR: Invalid path!", 401, session); + return; + } + uint64_t id = 0; + if(doc.FindMember("target_id") == doc.MemberEnd() || !doc["target_id"].IsUint64() || (id = doc["target_id"].GetUint64()) >= cfg.getTargetPaths().size()) { + sendResponse("ERROR: Invalid target_id!", 401, session); + return; + } + if(doc.FindMember("containing_dir") != doc.MemberEnd() && doc["containing_dir"].IsString()) { + containing_dir = doc["containing_dir"].GetString(); + } + std::string path = doc["path"].GetString(); + // TODO correct response code + sendResponse(moveJson(path, id, containing_dir), 200, session); +} + +void loginRest( const std::shared_ptr< restbed::Session > session, rapidjson::GenericDocument> &doc ) { + if(doc.FindMember("user") == doc.MemberEnd() || !doc["user"].IsString()) { + sendResponse("ERROR: Invalid user!", 401, session); + return; + } + if(doc.FindMember("password") == doc.MemberEnd() || !doc["password"].IsString()) { + sendResponse("ERROR: Invalid password!", 401, session); + return; + } + auto user = doc["user"].GetString(); + auto password = doc["password"].GetString(); + bool valid = false; + for(auto &user_cfg : cfg.getUsers()) { + if(user_cfg.first == user) { + valid = user_cfg.second == password; + break; + } + } + if(valid) { + sendResponse(createLoginToken(user), 200, session); + } else { + sendResponse("Invalid user/password", 401, session); + } +} + +void getOptionsCall(const std::shared_ptr session) { + performPostFunc(session, getOptionsRest); +} + +void getCustomKeysCall(const std::shared_ptr session) { + performPostFunc(session, getCustomKeysRest); +} + +void renameCall(const std::shared_ptr session) { + performPostFunc(session, renamePathRest); +} + +void getFilesCall(const std::shared_ptr session) { + performPostFunc(session, getFilesRest); +} + +void getTargetsCall(const std::shared_ptr session) { + performPostFunc(session, getTargetsRest); +} + +void getTargetDirectoriesCall(const std::shared_ptr session) { + performPostFunc(session, getTargetDirectoriesRest); +} + +void moveCall(const std::shared_ptr session) { + performPostFunc(session, moveRest); +} + +void loginCall(const std::shared_ptr session) { + performPostFunc(session, loginRest); +} + +int main(int argc, char **argv) { + cfg.readConfiguration("/etc/renameserver/main.cfg"); + + libraries = getLibraries(cfg.getLibraries()); + for(auto &library : libraries) { + library.init(library.config); + } + + restbed::Service service; + + auto get_types = std::make_shared< restbed::Resource >(); + get_types->set_path("/get_types"); + get_types->set_method_handler( "GET", getTypesRest ); + service.publish(get_types); + + auto search = std::make_shared< restbed::Resource >(); + search->set_path("/search"); + search->set_method_handler( "POST", getOptionsCall ); + service.publish(search); + + auto custom_fields = std::make_shared< restbed::Resource >(); + custom_fields->set_path("/get_custom_fields"); + custom_fields->set_method_handler( "POST", getCustomKeysCall ); + service.publish(custom_fields); + + auto rename_path = std::make_shared< restbed::Resource >(); + rename_path->set_path("/rename"); + rename_path->set_method_handler( "POST", renameCall ); + service.publish(rename_path); + + auto get_files = std::make_shared< restbed::Resource >(); + get_files->set_path("/get_files"); + get_files->set_method_handler( "POST", getFilesCall ); + service.publish(get_files); + + auto get_targets = std::make_shared< restbed::Resource >(); + get_targets->set_path("/get_targets"); + get_targets->set_method_handler( "POST", getTargetsCall ); + service.publish(get_targets); + + auto get_target_directories = std::make_shared< restbed::Resource >(); + get_target_directories->set_path("/get_target_directories"); + get_target_directories->set_method_handler( "POST", getTargetDirectoriesCall ); + service.publish(get_target_directories); + + auto move = std::make_shared< restbed::Resource >(); + move->set_path("/move"); + move->set_method_handler( "POST", moveCall ); + service.publish(move); + + auto login = std::make_shared< restbed::Resource >(); + login->set_path("/login"); + login->set_method_handler( "POST", loginCall ); + service.publish(login); + + auto settings = std::make_shared< restbed::Settings >(); + settings->set_port(1984); + settings->set_default_header( "Connection", "close" ); + + service.start(settings); + + return 0; +} diff --git a/network/network.hpp b/network/network.hpp new file mode 100644 index 0000000..6422af7 --- /dev/null +++ b/network/network.hpp @@ -0,0 +1,46 @@ +#ifndef NETWORK_HPP +#define NETWORK_HPP + +#ifdef _WIN32 + +#include +#include +#include + +using string = std::wstring; + +#else + +#include +#include + +using string = std::string; + +#endif + +class Request { +public: + Request(); + ~Request(); + std::string get( const string &url ); + std::string post( const string &url, const std::string &data ); + void addHeader( const string &header ); + void clearHeader(); + bool initSuccessful(); + void setServer( const string &server ); + int lastResponseCode(); + +private: +#ifdef _WIN32 + HINTERNET _hInternet; + HINTERNET _hConnect = nullptr; + std::wstring _headers = L""; + int status_code{}; +#else + CURL *_curl_handle = nullptr; + struct curl_slist *_chunk = nullptr; + string _server; +#endif +}; + +#endif diff --git a/network/unix/network.cpp b/network/unix/network.cpp new file mode 100644 index 0000000..505ba0c --- /dev/null +++ b/network/unix/network.cpp @@ -0,0 +1,91 @@ +#include "../network.hpp" +#include +#include + +size_t writeCallback( void *contents, size_t size, size_t nmemb, + void *target ) { + *static_cast< std::string * >( target ) += + std::string( static_cast< char * >( contents ), size * nmemb ); + return size * nmemb; +} + +Request::Request() { + curl_global_init( CURL_GLOBAL_ALL ); + _curl_handle = curl_easy_init(); + if ( _curl_handle == NULL ) { + std::cerr << "ERROR curl_easy_init" << std::endl; + } + curl_easy_setopt( _curl_handle, CURLOPT_WRITEFUNCTION, writeCallback ); + curl_easy_setopt( _curl_handle, CURLOPT_USERAGENT, "libcurl-agent/1.0" ); +} + +Request::~Request() { + curl_easy_cleanup( _curl_handle ); + curl_slist_free_all( _chunk ); + curl_global_cleanup(); +} + +void cleanUp( CURL *curl_handle ) { + curl_easy_setopt( curl_handle, CURLOPT_POST, 0 ); + curl_easy_setopt( curl_handle, CURLOPT_POSTFIELDS, "" ); +} + +std::string Request::get( const std::string &url ) { + // get rid of garbage + cleanUp( _curl_handle ); + std::string response; + response.reserve( 100000 ); + curl_easy_setopt( _curl_handle, CURLOPT_WRITEDATA, + static_cast< void * >( &response ) ); + curl_easy_setopt( _curl_handle, CURLOPT_URL, ( _server + url ).c_str() ); + curl_easy_setopt( _curl_handle, CURLOPT_HTTPGET, 1 ); + auto res = curl_easy_perform( _curl_handle ); + if ( res != CURLE_OK ) { + std::cerr << "curl_easy_perform() failed: " << curl_easy_strerror( res ) + << std::endl; + return ""; + } + return response; +} + +std::string Request::post( const std::string &url, const std::string &data ) { + std::string response; + response.reserve( 100000 ); + curl_easy_setopt( _curl_handle, CURLOPT_URL, ( _server + url ).c_str() ); + curl_easy_setopt( _curl_handle, CURLOPT_POST, 1 ); + curl_easy_setopt( _curl_handle, CURLOPT_POSTFIELDS, data.c_str() ); + curl_easy_setopt( _curl_handle, CURLOPT_WRITEDATA, + static_cast< void * >( &response ) ); + auto res = curl_easy_perform( _curl_handle ); + if ( res != CURLE_OK ) { + std::cerr << "curl_easy_perform() failed: " << curl_easy_strerror( res ) + << std::endl; + return ""; + } + return response; +} + +void Request::addHeader( const std::string &header ) { + _chunk = curl_slist_append( _chunk, header.c_str() ); + curl_easy_setopt( _curl_handle, CURLOPT_HTTPHEADER, _chunk ); +} + +void Request::clearHeader() { + curl_slist_free_all( _chunk ); + _chunk = nullptr; + curl_easy_setopt( _curl_handle, CURLOPT_HTTPHEADER, _chunk ); +} + +bool Request::initSuccessful() { + return _curl_handle != nullptr; +} + +void Request::setServer( const string &server ) { + _server = server; +} + +int Request::lastResponseCode() { + long code{}; + curl_easy_getinfo( _curl_handle, CURLINFO_RESPONSE_CODE, &code ); + return code; +} diff --git a/network/windows/network.cpp b/network/windows/network.cpp new file mode 100644 index 0000000..5b579e0 --- /dev/null +++ b/network/windows/network.cpp @@ -0,0 +1,109 @@ +#include "../network.hpp" +#include +#include + +const wchar_t *acceptTypes[] = { L"text/*", L"application/json", nullptr }; + +Request::Request() { + // Start connection + _hInternet = InternetOpen( L"WinInet/1.0", INTERNET_OPEN_TYPE_PRECONFIG, + nullptr, nullptr, 0 ); + if ( !_hInternet ) + std::wcerr << "ERROR InternetOpen: " << GetLastError() << std::endl; +} + +Request::~Request() { + if ( _hConnect ) + InternetCloseHandle( _hConnect ); + if ( _hInternet ) + InternetCloseHandle( _hInternet ); +} + +std::string sendRequest( HINTERNET hRequest, const std::wstring &headers, + const std::string &optional, int &status_code ) { + std::string response{}; + response.reserve( 10000 ); + + // have to do a copy because c_str returns const + std::unique_ptr< char > opt( new char[optional.length()] ); + memcpy( opt.get(), optional.c_str(), optional.length() ); + + auto requestSent = + HttpSendRequest( hRequest, headers.c_str(), headers.length(), opt.get(), + optional.length() ); + + if ( !requestSent ) { + status_code = -1; + std::wcerr << "ERROR HttpSendRequest: " << GetLastError() << std::endl; + return ""; + } + + char dataReceived[4096]; + unsigned long numberOfBytesRead = 0; + // read while InternetReadFile returns true and reads more than 0 bytes + while ( + InternetReadFile( hRequest, dataReceived, 4096, &numberOfBytesRead ) && + numberOfBytesRead ) { + response.append( dataReceived, numberOfBytesRead ); + } + + wchar_t responseText[256]; + DWORD responseTextSize = sizeof( responseText ); + + HttpQueryInfo( hRequest, HTTP_QUERY_STATUS_CODE, &responseText, + &responseTextSize, NULL ); + status_code = std::stoi( responseText ); + + return response; +} + +std::string Request::get( const std::wstring &url ) { + HINTERNET hRequest = + HttpOpenRequest( _hConnect, L"GET", url.c_str(), nullptr, nullptr, + acceptTypes, INTERNET_FLAG_SECURE, 0 ); + if ( !hRequest ) + std::wcerr << "ERROR HttpOpenRequest: " << GetLastError() << std::endl; + + return sendRequest( hRequest, _headers, "", status_code ); +} + +std::string Request::post( const std::wstring &url, const std::string &data ) { + HINTERNET hRequest = + HttpOpenRequest( _hConnect, L"POST", url.c_str(), nullptr, nullptr, + acceptTypes, INTERNET_FLAG_SECURE, 0 ); + if ( !hRequest ) + std::wcerr << "ERROR HttpOpenRequest: " << GetLastError() << std::endl; + + return sendRequest( hRequest, _headers, data, status_code ); +} + +void Request::addHeader( const std::wstring &header ) { + _headers += header + L"\r\n"; +} + +void Request::clearHeader() { + _headers = L""; +} + +bool Request::initSuccessful() { + return _hInternet != nullptr; +} + +void Request::setServer( const string &server ) { + // connect to server + _hConnect = InternetConnect( _hInternet, server.c_str(), + INTERNET_DEFAULT_HTTPS_PORT, nullptr, nullptr, + INTERNET_SERVICE_HTTP, 0, 0 ); + if ( !_hConnect ) + std::wcerr << "ERROR InternetConnect: " << GetLastError() << std::endl; +} + +int Request::lastResponseCode() { +#ifndef _WIN32 + long code{}; + curl_easy_getinfo( _curl_handle, CURLINFO_RESPONSE_CODE, &code ); + return code; +#else + return status_code; +#endif +} diff --git a/rename_library.hpp b/rename_library.hpp new file mode 100644 index 0000000..a5ad042 --- /dev/null +++ b/rename_library.hpp @@ -0,0 +1,17 @@ +#ifndef RENAME_LIBRARY_H +#define RENAME_LIBRARY_H + +#include +#include "rename_object.hpp" + +struct RenameLibrary { + bool ( *init )( const std::string & ); + std::vector< RenameObject > ( *getOptions )( const RenameObject & ); + bool ( *renamePath )( const std::string &, const RenameObject & ); + std::vector< std::string > ( *getCustomKeys )(); + void *libhndl; + std::string name; + std::string config; +}; + +#endif diff --git a/rename_object.cpp b/rename_object.cpp new file mode 100644 index 0000000..06ce687 --- /dev/null +++ b/rename_object.cpp @@ -0,0 +1,75 @@ +#include "rename_object.hpp" +#include +#include +#include + +void RenameObject::setLibraryId( uint64_t id ) { + _library_id = id; +} + +uint64_t RenameObject::getLibraryId() const { + return _library_id; +} + +void RenameObject::setPresentedName( const std::string &name ) { + _name = name; +} + +const std::string &RenameObject::getPresentedName() const { + return _name; +} + +void RenameObject::addCustomField( const std::string &key, + const std::string &value ) { + _custom_fields[key] = value; +} + +const std::unordered_map< std::string, std::string > &RenameObject::getCustomFields() const { + return _custom_fields; +} + +void RenameObject::clearCustomFields() { + _custom_fields.clear(); +} + +std::string RenameObject::toJson() const { + std::ostringstream result; + result << "{\n \"library_id\": " << _library_id << ",\n"; + result << " \"name\": \"" << _name << "\",\n"; + result << " \"custom_fields\": {\n"; + if ( _custom_fields.size() > 0 ) { + for ( auto &entry : _custom_fields ) { + result << " \"" << entry.first << "\": \"" << entry.second + << "\",\n"; + } + result.seekp( -2, std::ios_base::end ); + result << "\n"; + } + result << " }\n}"; + return result.str(); +} + +void invalidJson() { + throw "Invalid JSON"; +} + +void RenameObject::fromJson( const std::string &json ) { + rapidjson::Document doc; + doc.Parse( json.c_str() ); + if( doc.HasParseError() ) { + invalidJson(); + return; + } + if( doc.FindMember("library_id") != doc.MemberEnd() && doc["library_id"].IsUint64()) { + setLibraryId(doc["library_id"].GetUint64()); + } + if( doc.FindMember("name") != doc.MemberEnd() && doc["name"].IsString()) { + setPresentedName(doc["name"].GetString()); + } + if( doc.FindMember("custom_fields") != doc.MemberEnd() && doc["custom_fields"].IsObject()) { + auto custom_obj = doc["custom_fields"].GetObject(); + for(auto it = custom_obj.begin(); it != custom_obj.end(); it++) { + addCustomField(it->name.GetString(), it->value.GetString()); + } + } +} diff --git a/rename_object.hpp b/rename_object.hpp new file mode 100644 index 0000000..d958051 --- /dev/null +++ b/rename_object.hpp @@ -0,0 +1,25 @@ +#ifndef RENAME_OBJECT_H +#define RENAME_OBJECT_H + +#include +#include + +class RenameObject { +public: + void setLibraryId( uint64_t id ); + uint64_t getLibraryId() const; + void setPresentedName( const std::string &name ); + const std::string &getPresentedName() const; + void addCustomField( const std::string &key, const std::string &value ); + const std::unordered_map< std::string, std::string > &getCustomFields() const; + void clearCustomFields(); + std::string toJson() const; + void fromJson( const std::string &json ); + +private: + uint64_t _library_id; + std::string _name; + std::unordered_map< std::string, std::string > _custom_fields; +}; + +#endif diff --git a/simple_rename/simple.cpp b/simple_rename/simple.cpp new file mode 100644 index 0000000..fbe8c97 --- /dev/null +++ b/simple_rename/simple.cpp @@ -0,0 +1,51 @@ +#include "../filesystem/filesystem.hpp" +#include "../library.hpp" + +#include +#include +#include + +#ifdef _WIN32 + +#include + +#include +#include +#include +#include + +constexpr const char_t *_tv_rename_dir_divider = L"\\"; + +#else + +constexpr const char_t *_tv_rename_dir_divider = "/"; + +#endif + +bool init(const string &config_path) { + return true; +} + +std::vector< RenameObject > getOptions( const RenameObject &/*UNUSED*/ ) { + return {}; +} + +bool renamePath( const string &path, const RenameObject &renamer ) { + string new_name = ""; + + if ( renamer.getCustomFields().find( "new_name" ) != + renamer.getCustomFields().end() ) { + new_name = renamer.getCustomFields().at( "new_name" ); + } else { + return false; + } + + if ( new_name.find('/') != string::npos || new_name.find('\\') != string::npos ) { + return false; + } + return FSLib::rename( path, FSLib::canonical( FSLib::getContainingDirectory(path) ) + "/" + new_name ); +} + +std::vector< string > getCustomKeys() { + return { "new_name" }; +} diff --git a/themoviedb/functions.cpp b/themoviedb/functions.cpp new file mode 100644 index 0000000..1911cfe --- /dev/null +++ b/themoviedb/functions.cpp @@ -0,0 +1,87 @@ +#include +#include +#include +#include +#include + +#include "functions.hpp" + +#ifdef _WIN32 + +#include +#include + +#define cout std::wcout +#define cerr std::wcerr +#define cin std::wcin + +constexpr const char_t *dir_divider = L"\\"; + +#else // UNIX + +#include +#include +#include +#include + +#define cout std::cout +#define cerr std::cerr +#define cin std::cin + +constexpr const char_t *dir_divider = "/"; + +#endif // UNIX + +#ifdef __APPLE__ +void error( int status, int i_errno, const char *fmt, ... ) { + fprintf( stderr, "%s - ", strerror( i_errno ) ); + va_list args; + va_start( args, fmt ); + vfprintf( stderr, fmt, args ); + va_end( args ); + fputc( '\n', stderr ); + exit( status ); +} +#endif + +#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 == '~' || x == '+' ) { + encoded << x; + continue; + } + encoded << std::uppercase << '%' << std::setw( 2 ); + encoded << int( static_cast< unsigned char >( x ) ) << std::nouppercase; + } + return encoded.str(); +} + diff --git a/themoviedb/functions.hpp b/themoviedb/functions.hpp new file mode 100644 index 0000000..f4db70d --- /dev/null +++ b/themoviedb/functions.hpp @@ -0,0 +1,35 @@ +#ifndef TV_FUNCTIONS_H +#define TV_FUNCTIONS_H + +#include +#include +#include +#include +#include +#include + +#include "../network/network.hpp" + +#ifdef _WIN32 + +using string = std::wstring; +using char_t = wchar_t; + +std::wstring utf8_to_wstring( const std::string &utf8 ); + +#else + +using string = std::string; +using char_t = char; + +#define TEXT( x ) x + +#endif + +#ifdef __APPLE__ +void error( int status, int i_errno, const char *fmt, ... ); +#endif + +// CLI functions +string encodeUrl( const string &url ); +#endif diff --git a/themoviedb/moviedb.cpp b/themoviedb/moviedb.cpp new file mode 100644 index 0000000..ca3de8d --- /dev/null +++ b/themoviedb/moviedb.cpp @@ -0,0 +1,200 @@ +// API - 2ebc8e784a4072da457fae5c0d291e48 +// API READ ONLY - eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiIyZWJjOGU3ODRhNDA3MmRhNDU3ZmFlNWMwZDI5MWU0OCIsInN1YiI6IjYwZTJlNGI5MjJlNDgwMDA2MDJmZDMzMyIsInNjb3BlcyI6WyJhcGlfcmVhZCJdLCJ2ZXJzaW9uIjoxfQ.c0y7bTCI5KSsfQRw7igPx1FR40mbMF6hGTJTHn0HXH8 +#include +#include +#include + +#include "../filesystem/filesystem.hpp" +#include "functions.hpp" +#include "../library.hpp" + +#ifdef _WIN32 + +#include "rapidjson/document.h" + +#else + +#include + +#endif + +#include +#include + +#ifdef _WIN32 + +#include + +#include +#include +#include +#include + +constexpr const char_t *_tv_rename_dir_divider = L"\\"; + +#define toString( a ) utf8_to_wstring( a ) + +#else + +constexpr const char_t *_tv_rename_dir_divider = "/"; + +#define toString( a ) a + +#endif + +string _moviedb_api_token{}; +Request _moviedb_request; + +bool init(const string &config_path) { + Request &request = _moviedb_request; +#ifdef _WIN32 + request.setServer( TEXT( "api.themoviedb.org/3" ) ); +#else + request.setServer( "https://api.themoviedb.org/3" ); +#endif + _moviedb_api_token = "eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiIyZWJjOGU3ODRhNDA3MmRhNDU3ZmFlNWMwZDI5MWU0OCIsInN1YiI6IjYwZTJlNGI5MjJlNDgwMDA2MDJmZDMzMyIsInNjb3BlcyI6WyJhcGlfcmVhZCJdLCJ2ZXJzaW9uIjoxfQ.c0y7bTCI5KSsfQRw7igPx1FR40mbMF6hGTJTHn0HXH8"; + return true; +} + +std::vector< std::tuple< string, string, string, string > > +searchMovie( const string &movie, const string &language, const string &year ) { + Request &request = _moviedb_request; + request.addHeader( TEXT( "Accept: application/json" ) ); + request.addHeader( TEXT( "Authorization: Bearer " ) + _moviedb_api_token ); + + auto encoded_show = encodeUrl( movie ); + int pages = 0; + int cur_page = 0; + + std::vector< std::tuple< string, string, string, string > > ret; + + do { + cur_page++; + rapidjson::Document json; + auto request_uri = TEXT( "/search/movie?query=" ) + encoded_show + TEXT("&language=" ) + language + TEXT("&page=") + toString(std::to_string(cur_page)); + if(!year.empty()) { + request_uri += TEXT("&year=") + year; + } + json.Parse( + request.get( request_uri ).c_str() ); + if ( json.HasParseError() ) + return {}; + + pages = json["total_pages"].GetInt(); + + const rapidjson::Value &results = json["results"]; + if ( !results.IsArray() ) { + return {}; + } + + // find all possible movies + for ( size_t i = 0; i < results.Size(); i++ ) { + auto movie = toString( results[i]["title"].GetString() ); + auto id = toString( std::to_string( results[i]["id"].GetInt() ) ); + string year = toString( results[i]["release_date"].GetString() ); + string original = toString( results[i]["original_title"].GetString() ); + if(year.empty() ) { + year = "0000"; + } else { + year = year.substr(0, year.find('-')); + } + ret.emplace_back( movie, id, year, original ); + } + } while(cur_page < pages && cur_page < 5); + request.clearHeader(); + return ret; +} + +RenameObject movieToRenameObject( const std::tuple< string, string, string, string > &movie, + const std::string &language ) { + RenameObject result; + result.setPresentedName( std::get<0>(movie) ); + result.addCustomField( "id", std::get<1>(movie) ); + result.addCustomField( "language", language ); + result.addCustomField( "year", std::get<2>(movie) ); + result.addCustomField( "original_title", std::get<3>(movie) ); + result.addCustomField( "use_original", "false" ); + return result; +} + +std::vector< RenameObject > getOptions( const RenameObject &search ) { + string lang = "en-US"; + string year = ""; + if ( search.getCustomFields().find( "language" ) != + search.getCustomFields().end() ) { + lang = search.getCustomFields().at( "language" ); + } + if ( search.getCustomFields().find( "year" ) != + search.getCustomFields().end() ) { + year = search.getCustomFields().at( "year" ); + } + string name = search.getPresentedName(); + auto possibilities = searchMovie( name, lang, year ); + std::vector< RenameObject > result{}; + for ( auto &movie : possibilities ) { + result.push_back( movieToRenameObject( movie, lang ) ); + } + return result; +} + +std::pair movieFromId( const string &id, const string &language ) { + string uri = "/movie/" + id + "?language=" + language; + + Request &request = _moviedb_request; + request.addHeader( TEXT( "Accept: application/json" ) ); + request.addHeader( TEXT( "Authorization: Bearer " ) + _moviedb_api_token ); + rapidjson::Document json; + json.Parse( request.get( uri ).c_str() ); + if( json.HasParseError() ) { + return {"", ""}; + } + return {json["title"].GetString(), json["original_title"].GetString()}; +} + +bool renameMovie(const string &path, const string &name, const string &year) { + return FSLib::rename(path, FSLib::canonical( FSLib::getContainingDirectory(path) ) + "/" + name + " (" + year + ")." + FSLib::getFileExtension(path) ); +} + +bool renamePath( const string &path, const RenameObject &renamer ) { + string id = ""; + string lang = "en-US"; + std::pair movie = {"",""}; + string year = ""; + bool use_original = false; + + if ( renamer.getCustomFields().find( "language" ) != + renamer.getCustomFields().end() ) { + lang = renamer.getCustomFields().at( "language" ); + } + + if ( renamer.getCustomFields().find( "year" ) != + renamer.getCustomFields().end() ) { + year = renamer.getCustomFields().at( "year" ); + } + + if ( renamer.getCustomFields().find( "use_original" ) != + renamer.getCustomFields().end() ) { + auto use = renamer.getCustomFields().at( "use_original" ); + use_original = (use == "true" || use == "True" || use == "TRUE"); + } + + if ( renamer.getCustomFields().find( "id" ) == + renamer.getCustomFields().end() || + renamer.getCustomFields().at( "id" ).empty() ) { + auto results = searchMovie( renamer.getPresentedName(), lang, year ); + if ( results.empty() ) + return false; + id = std::get<1>(results[0]); + movie = { std::get<0>(results[0]), std::get<3>(results[0]) }; + year = std::get<2>(results[0]); + } else { + id = renamer.getCustomFields().at( "id" ); + movie = movieFromId( id, lang ); + } + + return renameMovie( path, use_original ? movie.second : movie.first, year ); +} + +std::vector< string > getCustomKeys() { + return { "id", "language", "year", "original_title", "use_original" }; +} diff --git a/thetvdb/functions.cpp b/thetvdb/functions.cpp new file mode 100644 index 0000000..f7e4457 --- /dev/null +++ b/thetvdb/functions.cpp @@ -0,0 +1,255 @@ +#include +#include +#include +#include +#include + +#include "../filesystem/filesystem.hpp" +#include "functions.hpp" + +#ifdef _WIN32 + +#include +#include + +#define cout std::wcout +#define cerr std::wcerr +#define cin std::wcin + +constexpr const char_t *dir_divider = L"\\"; + +#else // UNIX + +#include +#include +#include +#include + +#define cout std::cout +#define cerr std::cerr +#define cin std::cin + +constexpr const char_t *dir_divider = "/"; + +#endif // UNIX + +#ifdef __APPLE__ +void error( int status, int i_errno, const char *fmt, ... ) { + fprintf( stderr, "%s - ", strerror( i_errno ) ); + va_list args; + va_start( args, fmt ); + vfprintf( stderr, fmt, args ); + va_end( args ); + fputc( '\n', stderr ); + exit( status ); +} +#endif + +#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 == '~' || 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 file contains S[0-9]+E[0-9]+, set +// season_pos to start of season number and ep_pos to start of episode number +bool searchSeason( const char_t *const path, size_t &season_pos, + size_t &ep_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++; + if ( path[cur_pos] == '\0' ) + return false; + 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] == '\0' ) + return false; + 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; +} + +void iterateFS( std::map< int, std::map< int, string > > &seasons, + const string &path ) { + // season_pos - position of first digit of the season number + // ep_pos - position of first digit of the episode number + size_t season_pos{ string::npos }; + size_t ep_pos{ string::npos }; + if ( FSLib::isDirectory( path ) ) { + 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, ep_pos ) ) + seasons[std::stoi( p + season_pos )][std::stoi( p + ep_pos )] = + path + dir_divider + p; + } + } else if ( searchSeason( path.c_str(), season_pos, ep_pos ) ) { + seasons[std::stoi( path.c_str() + season_pos )][std::stoi( path.c_str() + ep_pos )] = path; + } +} + +// 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; +} + +/* change names of original files to generated new names + * orig - original filenames + * renamed - renamed filenames (sorted in the same order as `orig`) + */ +void renameFiles( const std::vector< std::tuple< int, string, string, string > > + &renamed_files ) { + for ( const auto &renamed : renamed_files ) { + FSLib::rename( + std::get< 1 >( renamed ) + dir_divider + std::get< 2 >( renamed ), + std::get< 1 >( renamed ) + dir_divider + std::get< 3 >( renamed ) ); + } +} + +// TODO read config +const string getDefaultPattern() { + return "S%2seasonE%2episode - %epname"; +} diff --git a/thetvdb/functions.hpp b/thetvdb/functions.hpp new file mode 100644 index 0000000..bc4e367 --- /dev/null +++ b/thetvdb/functions.hpp @@ -0,0 +1,44 @@ +#ifndef TV_FUNCTIONS_H +#define TV_FUNCTIONS_H + +#include +#include +#include +#include +#include +#include + +#include "../network/network.hpp" + +#ifdef _WIN32 + +using string = std::wstring; +using char_t = wchar_t; + +std::wstring utf8_to_wstring( const std::string &utf8 ); + +#else + +using string = std::string; +using char_t = char; + +#define TEXT( x ) x + +#endif + +#ifdef __APPLE__ +void error( int status, int i_errno, const char *fmt, ... ); +#endif + +// CLI functions +const string getDefaultPattern(); + +string encodeUrl( const string &url ); + +void iterateFS( std::map< int, std::map< int, string > > &seasons, + const string &path ); + +string compilePattern( const string &pattern, int season, int episode, + const string &filename, const string &episodeName, + const string &showName ); +#endif diff --git a/thetvdb/tv_rename.cpp b/thetvdb/tv_rename.cpp new file mode 100644 index 0000000..3da0509 --- /dev/null +++ b/thetvdb/tv_rename.cpp @@ -0,0 +1,414 @@ +#include +#include +#include + +#include "../filesystem/filesystem.hpp" +#include "functions.hpp" +#include "../library.hpp" + +#ifdef _WIN32 + +#include "rapidjson/document.h" + +#else + +#include + +#endif + +#include +#include + +#ifdef _WIN32 + +#include + +#include +#include +#include +#include + +constexpr const char_t *_tv_rename_dir_divider = L"\\"; + +#define cout std::wcout +#define cerr std::wcerr +#define cin std::wcin + +#define toString( a ) utf8_to_wstring( a ) + +#else + +constexpr const char_t *_tv_rename_dir_divider = "/"; + +#define cout std::cout +#define cerr std::cerr +#define cin std::cin + +#define toString( a ) a + +#endif + +// TV flags +#define TV_CHDIR 0x0100 +#define TV_TRUST 0x0200 +#define TV_LINUX 0x0400 +#define TV_DVD 0x0800 + +string _tv_rename_api_token{}; +Request _tv_rename_request; + +bool authenticate( const std::string &api_key ) { + Request &request = _tv_rename_request; +#ifdef _WIN32 + request.setServer( TEXT( "api.thetvdb.com" ) ); +#else + request.setServer( "https://api.thetvdb.com" ); +#endif + request.addHeader( TEXT( "Accept: application/json" ) ); + request.addHeader( TEXT( "Content-Type: application/json" ) ); + rapidjson::Document json; + json.Parse( + request.post( TEXT( "/login" ), "{ \"apikey\": \"" + api_key + "\" }" ) + .c_str() ); + if ( json.HasParseError() ) + return false; + + _tv_rename_api_token = toString( json["token"].GetString() ); + request.clearHeader(); + // TODO check return code + return true; +} + +bool init(const string &config_path) { + return authenticate( "42B66F5E-C6BF-423F-ADF9-CC97163472F6" ); +} + +std::vector< std::pair< string, string > > +searchShow( const string &show, const string &language ) { + Request &request = _tv_rename_request; + request.addHeader( TEXT( "Accept: application/json" ) ); + request.addHeader( TEXT( "Authorization: Bearer " ) + + _tv_rename_api_token ); + request.addHeader( TEXT( "Accept-Language: " ) + language ); + + auto encoded_show = encodeUrl( show ); + + rapidjson::Document json; + json.Parse( + request.get( TEXT( "/search/series?name=" ) + encoded_show ).c_str() ); + if ( json.HasParseError() ) + return {}; + + const rapidjson::Value &results = json["data"]; + if ( !results.IsArray() ) { + return {}; + } + + std::vector< std::pair< string, string > > ret; + + // find all possible shows + for ( size_t i = 0; i < results.Size(); i++ ) { + auto show = toString( results[i]["seriesName"].GetString() ); + auto id = toString( std::to_string( results[i]["id"].GetInt() ) ); + ret.emplace_back( show, id ); + } + request.clearHeader(); + return ret; +} + +// get names for all episodes for a given season +std::vector< string > getEpisodeNames( const string &id, const string &season, + const string &language, + bool dvd = false ) { + Request &request = _tv_rename_request; + request.addHeader( TEXT( "Accept: application/json" ) ); + request.addHeader( TEXT( "Authorization: Bearer " ) + + _tv_rename_api_token ); + request.addHeader( TEXT( "Accept-Language: " ) + language ); + + string baseurl = TEXT( "/series/" ) + id + TEXT( "/episodes/query?" ); + if ( dvd ) + baseurl += TEXT( "dvdSeason=" ); + else + baseurl += TEXT( "airedSeason=" ); + baseurl += season; + baseurl += TEXT( "&page=" ); + string page = TEXT( "1" ); + + std::vector< string > episodes; + + do { + episodes.resize( episodes.size() * 2 ); + rapidjson::Document json; + json.Parse( request.get( baseurl + page ).c_str() ); + if ( json.HasParseError() ) + return {}; + if ( json.FindMember( "data" ) == json.MemberEnd() ) + break; + + if ( json["data"].IsArray() ) { + rapidjson::Value &epdata = json["data"]; + if ( episodes.size() < epdata.Size() ) + episodes.resize( epdata.Size() ); + + for ( size_t i = 0; i < epdata.Size(); i++ ) { + if ( epdata[i]["episodeName"].IsString() ) { + size_t index = -1; + if ( dvd ) + index = epdata[i]["dvdEpisodeNumber"].GetInt(); + else + index = epdata[i]["airedEpisodeNumber"].GetInt(); + + if ( index == static_cast< size_t >( -1 ) ) + continue; + + if ( index > episodes.size() ) + episodes.resize( index ); + index--; + episodes[index] = + toString( epdata[i]["episodeName"].GetString() ); + // some eps have whitespace at the end + while ( isspace( episodes[index].back() ) ) + episodes[index].pop_back(); + } + } + } + if ( json["links"]["next"].IsNull() ) + break; + page = toString( std::to_string( json["links"]["next"].GetInt() ) ); + } while ( 1 ); + request.clearHeader(); + return episodes; +} + +std::vector< std::tuple< int, string, string, string > > +getRenamedFiles( const string &show, int season, const string &id, + const string &language, const string &pattern, + const bool &unix_names, const std::map< int, string > &files, + bool dvd ) { + auto season_num = toString( std::to_string( season ) ); + auto episodes = getEpisodeNames( id, season_num, language, dvd ); + + if ( episodes.empty() ) + return {}; + + if ( files.empty() ) + return {}; + + // vector of pairs , + std::vector< std::tuple< int, string, string, string > > renamed_files; + + for ( const auto &x : files ) { + auto last = x.second.find_last_of( _tv_rename_dir_divider ); + string og_name; + string dir; + if ( last == string::npos ) { + og_name = x.second; + dir = TEXT( "." ); + } else { + og_name = x.second.substr( last + 1 ); + dir = x.second.substr( 0, last ); + } + unsigned long ep_num = x.first - 1; + + if ( ep_num < episodes.size() ) { + auto pos = og_name.find_last_of( TEXT( "." ) ); + string og_name_without_extension{}; + string og_name_extension{}; + + if ( pos == string::npos ) { + og_name_without_extension = og_name; + } else { + og_name_without_extension = og_name.substr( 0, pos ); + og_name_extension = og_name.substr( pos ); + } + + // get desired filename + auto name = compilePattern( pattern, season, x.first, + og_name_without_extension, + episodes[ep_num], show ) + + og_name_extension; + // replace '/' with '|' + for ( size_t i = 0; i < name.size(); i++ ) { + if ( name[i] == '/' ) { + name[i] = '|'; + } + } + // replace characters illegal in windows if desired + if ( !unix_names ) { + name.erase( std::remove_if( name.begin(), name.end(), + []( char_t x ) { + return x == '?' || x == '"' || + x == '\\' || x == '*'; + } ), + name.end() ); + size_t max{ name.size() }; + for ( size_t i = 0; i < max; i++ ) { + if ( name[i] == '|' ) { + name[i] = '-'; + } else if ( name[i] == '<' ) { + name.erase( i, 1 ); + name.insert( i, TEXT( "<" ) ); + max = name.size(); + } else if ( name[i] == '>' ) { + name.erase( i, 1 ); + name.insert( i, TEXT( ">" ) ); + max = name.size(); + } else if ( name[i] == ':' ) { + name[i] = ' '; + name.insert( i + 1, TEXT( "- " ) ); + max = name.size(); + } + } + for ( size_t i = 0; i < max; i++ ) { + if ( name[i] == ' ' && name[i + 1] == ' ' ) { + name.erase( i, 1 ); + max--; + i--; + } + } + } + renamed_files.emplace_back( x.first, dir, og_name, name ); + } + } + return renamed_files; +} + +string showFromId( const string &id ) { + Request &request = _tv_rename_request; + request.addHeader( TEXT( "Accept: application/json" ) ); + request.addHeader( TEXT( "Authorization: Bearer " ) + + _tv_rename_api_token ); + + rapidjson::Document json; + json.Parse( request.get( TEXT( "/series/" ) + id ).c_str() ); + if ( json.HasParseError() ) { + return ""; + } + return json["data"]["seriesName"].GetString(); +} + +void singleSeason( const string &path, const string &show, int season, + string id, const string &language, const string &pattern, + const bool &unix_names, const bool &trust, + std::map< int, string > *files_ptr, bool dvd ) { + + std::map< int, std::map< int, string > > *found_files = nullptr; + + if ( files_ptr == nullptr ) { + found_files = new std::map< int, std::map< int, string > >; + iterateFS( *found_files, path ); + if ( found_files->find( season ) != found_files->end() ) + files_ptr = &( *found_files )[season]; + } + + if ( files_ptr == nullptr ) { + return; + } + + auto renamed_files = getRenamedFiles( show, season, id, language, pattern, + unix_names, *files_ptr, dvd ); + + if ( renamed_files.empty() ) + goto end; + + for ( const auto &renamed : renamed_files ) { + FSLib::rename( std::get< 1 >( renamed ) + _tv_rename_dir_divider + + std::get< 2 >( renamed ), + std::get< 1 >( renamed ) + _tv_rename_dir_divider + + std::get< 3 >( renamed ) ); + if ( found_files == nullptr ) { + files_ptr[0][std::get< 0 >( renamed )] = std::get< 1 >( renamed ) + + _tv_rename_dir_divider + + std::get< 3 >( renamed ); + } + } + +end: + if ( found_files != nullptr ) { + delete found_files; + } +} + +void allSeasons( const string &id, const string &path, const string &show, + const string &language, const string &pattern, + const size_t &flags ) { + std::map< int, std::map< int, string > > seasons; + // get all season number from this directory and subdirectories + iterateFS( seasons, path ); + for ( auto &x : seasons ) { + singleSeason( path, show, x.first, id, language, pattern, + flags & TV_LINUX, flags & TV_TRUST, &x.second, + flags & TV_DVD ); + } +} + +RenameObject showToRenameObject( const std::pair< string, string > &show, + const std::string &language ) { + RenameObject result; + result.setPresentedName( show.first ); + result.addCustomField( "id", show.second ); + result.addCustomField( "language", language ); + result.addCustomField( "order", "aired" ); + result.addCustomField( "pattern", getDefaultPattern() ); + return result; +} + +std::vector< RenameObject > getOptions( const RenameObject &search ) { + string lang = "en"; + if ( search.getCustomFields().find( "language" ) != + search.getCustomFields().end() ) { + lang = search.getCustomFields().at( "language" ); + } + string name = search.getPresentedName(); + auto possibilities = searchShow( name, lang ); + std::vector< RenameObject > result{}; + for ( auto &show : possibilities ) { + result.push_back( showToRenameObject( show, lang ) ); + } + return result; +} + +bool renamePath( const string &path, const RenameObject &renamer ) { + string id = ""; + string lang = "en"; + string pattern = getDefaultPattern(); + string show = ""; + size_t flags = TV_TRUST; + + if ( renamer.getCustomFields().find( "language" ) != + renamer.getCustomFields().end() ) { + lang = renamer.getCustomFields().at( "language" ); + } + + if ( renamer.getCustomFields().find( "id" ) == + renamer.getCustomFields().end() || + renamer.getCustomFields().at( "id" ).empty() ) { + auto results = searchShow( renamer.getPresentedName(), lang ); + if ( results.empty() ) + return false; + id = results[0].second; + show = results[0].first; + } else if( renamer.getCustomFields().find("id") != renamer.getCustomFields().end()) { + id = renamer.getCustomFields().at( "id" ); + show = showFromId( id ); + } + + if ( renamer.getCustomFields().find( "pattern" ) != + renamer.getCustomFields().end() ) { + pattern = renamer.getCustomFields().at( "pattern" ); + } + + if ( renamer.getCustomFields().find( "order" ) != + renamer.getCustomFields().end() && + renamer.getCustomFields().at( "order" ) == "dvd" ) { + flags |= TV_DVD; + } + + allSeasons( id, path, show, lang, pattern, flags ); + return true; +} + +std::vector< string > getCustomKeys() { + return { "id", "language", "pattern", "order" }; +}