diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b9df4e5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +__pycache__ +.venv +config.ini diff --git a/config.ini b/config.ini deleted file mode 100644 index 31d2057..0000000 --- a/config.ini +++ /dev/null @@ -1,20 +0,0 @@ -[TargetedCreator] -username = ReplaceMe - -[MyAccount] -authorization_token = ReplaceMe -user_agent = ReplaceMe - -[Options] -download_mode = Normal -show_downloads = True -download_media_previews = True -open_folder_when_finished = True -download_directory = Local_directory -separate_messages = True -separate_previews = False -separate_timeline = True -utilise_duplicate_threshold = False - -[Other] -version = 0.4.1 diff --git a/config.ini.example b/config.ini.example new file mode 100644 index 0000000..9e56ab0 --- /dev/null +++ b/config.ini.example @@ -0,0 +1,21 @@ +[TargetedCreator] +username = CREATOR HERE + +[MyAccount] +authorization_token = YOUR TOKEN HERE +user_agent = Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36 + +[Options] +download_mode = Normal +show_downloads = True +download_media_previews = True +open_folder_when_finished = False +download_directory = YOUR DIRECTORY HERE +separate_messages = True +separate_previews = True +separate_timeline = True +utilise_duplicate_threshold = False + +[Other] +version = 0.4.1 + diff --git a/fansly_downloader.py b/fansly_downloader.py index f2b2164..b6b38b5 100644 --- a/fansly_downloader.py +++ b/fansly_downloader.py @@ -1,5 +1,5 @@ # fix in future: audio needs to be properly transcoded from mp4 to mp3, instead of just saved as -import requests, os, re, base64, hashlib, imagehash, io, traceback, sys, platform, subprocess, concurrent.futures, json, m3u8, av, time, mimetypes, configparser +import requests, os, re, base64, hashlib, imagehash, io, traceback, sys, platform, subprocess, concurrent.futures, json, m3u8, av, time, mimetypes, configparser, argparse from random import randint from tkinter import Tk, filedialog from loguru import logger as log @@ -13,6 +13,7 @@ from os import makedirs, getcwd from utils.update_util import delete_deprecated_files, check_latest_release, apply_old_config_values + # tell PIL to be tolerant of files that are truncated ImageFile.LOAD_TRUNCATED_IMAGES = True @@ -22,6 +23,20 @@ # define requests session sess = requests.Session() +# CLI Arg setup +parser = argparse.ArgumentParser( + prog="fansly_downloader", + description='''Download a creator's picures and videos from fansly. + Confiuration is read from config.ini, and can be + overridden by the following options''', + epilog="Thanks for using %(prog)s! :)", + allow_abbrev=False +) +parser.add_argument("-c","--creator", action="store", help="The Name of the Creator to Download") +parser.add_argument("-u","--upgrade", action="store_true", help="Upgrade config file to the new version") +parser.add_argument("-d","--download", action="store", help="Download Mode. One of Normal, Timeline, Messages, Single (Single by post id) or Collections") +parser.add_argument("-b","--batch", action="store_false", help="Batch mode, do not prompt on Exit") +args = parser.parse_args() # cross-platform compatible, re-name downloaders terminal output window title def set_window_title(title): @@ -33,7 +48,9 @@ def set_window_title(title): set_window_title('Fansly Downloader') # for pyinstaller compatibility -def exit(): +def exit(msg): + if args.batch: + input(msg) os._exit(0) # base64 code to display logo in console @@ -64,13 +81,12 @@ def open_url(url_to_open: str): config_path = join(getcwd(), 'config.ini') if len(config.read(config_path)) != 1: output(2,'\n [1]ERROR','', f"config.ini file not found or can not be read.\n{21*' '}Please download it & make sure it is in the same directory as fansly downloader") - input('\nPress Enter to close ...') - exit() + exit('\nPress Enter to close ...') ## starting here: self updating functionality # if started with --update start argument -if len(sys.argv) > 1 and sys.argv[1] == '--update': +if args.upgrade: # config.ini backwards compatibility fix (≤ v0.4) -> fix spelling mistake "seperate" to "separate" if 'seperate_messages' in config['Options']: config['Options']['separate_messages'] = config['Options'].pop('seperate_messages') @@ -104,21 +120,25 @@ def open_url(url_to_open: str): # read the config.ini file for a last time config.read(config_path) else: - # check if a new version is available check_latest_release(current_version = config.get('Other', 'version'), intend = 'check') - ## read & verify config values try: # TargetedCreator - config_username = config.get('TargetedCreator', 'Username') # string + if args.creator == "": + config_username = config.get('TargetedCreator', 'Username') # string + else: + config_username = args.creator # MyAccount config_token = config.get('MyAccount', 'Authorization_Token') # string config_useragent = config.get('MyAccount', 'User_Agent') # string # Options - download_mode = config.get('Options', 'download_mode').capitalize() # Normal (Timeline & Messages), Timeline, Messages, Single (Single by post id) or Collections -> str + if args.download == "": + download_mode = config.get('Options', 'download_mode').capitalize() # Normal (Timeline & Messages), Timeline, Messages, Single (Single by post id) or Collections -> str + else: + download_mode = args.download show_downloads = config.getboolean('Options', 'show_downloads') # True, False -> boolean download_media_previews = config.getboolean('Options', 'download_media_previews') # True, False -> boolean open_folder_when_finished = config.getboolean('Options', 'open_folder_when_finished') # True, False -> boolean @@ -133,28 +153,24 @@ def open_url(url_to_open: str): except configparser.NoOptionError as e: error_string = str(e) output(2,'\n ERROR','', f"Your config.ini file is very malformed, please download a fresh version of it from GitHub.\n{error_string}") - input('\nPress Enter to close ...') - exit() + exit('\nPress Enter to close ...') except ValueError as e: error_string = str(e) if 'a boolean' in error_string: output(2,'\n [1]ERROR','', f"\'{error_string.rsplit('boolean: ')[1]}\' is malformed in the configuration file! This value can only be True or False\n\ {6*' '}Read the Wiki > Explanation of provided programs & their functionality > config.ini") open_url('https://github.com/Avnsx/fansly-downloader/wiki/Explanation-of-provided-programs-&-their-functionality#4-configini') - input('\nPress Enter to close ...') - exit() + exit('\nPress Enter to close ...') else: output(2,'\n [2]ERROR','', f"You have entered a wrong value in the config.ini file -> \'{error_string}\'\n\ {6*' '}Read the Wiki > Explanation of provided programs & their functionality > config.ini") open_url('https://github.com/Avnsx/fansly-downloader/wiki/Explanation-of-provided-programs-&-their-functionality#4-configini') - input('\nPress Enter to close ...') - exit() + exit('\nPress Enter to close ...') except (KeyError, NameError) as key: output(2,'\n [3]ERROR','', f"\'{key}\' is missing or malformed in the configuration file!\n\ {6*' '}Read the Wiki > Explanation of provided programs & their functionality > config.ini") open_url('https://github.com/Avnsx/fansly-downloader/wiki/Explanation-of-provided-programs-&-their-functionality#4-configini') - input('\nPress Enter to close ...') - exit() + exit('\nPress Enter to close ...') # update window title with specific downloader version @@ -325,16 +341,14 @@ def remind_stargazing(): {10*' '}Did you not recently browse Fansly with an authenticated session?\ {10*' '}Please read & apply the \'Get-Started\' tutorial instead.") open_url('https://github.com/Avnsx/fansly-downloader/wiki/Get-Started') - input('\n Press Enter to close ..') - exit() + exit('\n Press Enter to close ..') # if users decisions have led to auth token still being invalid elif any([not config_token, 'ReplaceMe' in config_token]) or config_token and len(config_token) < 50: output(2,'\n ERROR','', f"Reached the end and the authentication token in config.ini file is still invalid!\n\ {10*' '}Please read & apply the \'Get-Started\' tutorial instead.") open_url('https://github.com/Avnsx/fansly-downloader/wiki/Get-Started') - input('\n Press Enter to close ..') - exit() + exit('\n Press Enter to close ..') # validate input value for "user_agent" in config.ini @@ -459,20 +473,20 @@ def generate_base_dir(creator_name_to_create_for: str, module_requested_by: str) if "Collection" in module_requested_by: BASE_DIR_NAME = join(getcwd(), 'Collections') elif "Message" in module_requested_by and separate_messages: - BASE_DIR_NAME = join(getcwd(), creator_name_to_create_for+'_fansly', 'Messages') + BASE_DIR_NAME = join(getcwd(), creator_name_to_create_for, 'Messages') elif "Timeline" in module_requested_by and separate_timeline: - BASE_DIR_NAME = join(getcwd(), creator_name_to_create_for+'_fansly', 'Timeline') + BASE_DIR_NAME = join(getcwd(), creator_name_to_create_for, 'Timeline') else: - BASE_DIR_NAME = join(getcwd(), creator_name_to_create_for+'_fansly') # use local directory + BASE_DIR_NAME = join(getcwd(), creator_name_to_create_for) # use local directory elif os.path.isdir(download_directory): # if user specified a correct custom downloads path if "Collection" in module_requested_by: BASE_DIR_NAME = join(download_directory, 'Collections') elif "Message" in module_requested_by and separate_messages: - BASE_DIR_NAME = join(download_directory, creator_name_to_create_for+'_fansly', 'Messages') + BASE_DIR_NAME = join(download_directory, creator_name_to_create_for, 'Messages') elif "Timeline" in module_requested_by and separate_timeline: - BASE_DIR_NAME = join(download_directory, creator_name_to_create_for+'_fansly', 'Timeline') + BASE_DIR_NAME = join(download_directory, creator_name_to_create_for, 'Timeline') else: - BASE_DIR_NAME = join(download_directory, creator_name_to_create_for+'_fansly') # use their custom path & specify new folder for the current creator in it + BASE_DIR_NAME = join(download_directory, creator_name_to_create_for) # use their custom path & specify new folder for the current creator in it output(1,' Info','', f"Acknowledging custom basis download directory: \'{download_directory}\'") else: # if their set directory, can't be found by the OS output(3,'\n WARNING','', f"The custom basis download directory file path: \'{download_directory}\'; seems to be invalid!\ @@ -490,14 +504,14 @@ def generate_base_dir(creator_name_to_create_for: str, module_requested_by: str) if "Collection" in module_requested_by: BASE_DIR_NAME = join(download_directory, 'Collections') elif "Message" in module_requested_by and separate_messages: - BASE_DIR_NAME = join(download_directory, creator_name_to_create_for+'_fansly', 'Messages') + BASE_DIR_NAME = join(download_directory, creator_name_to_create_for, 'Messages') elif "Timeline" in module_requested_by and separate_timeline: - BASE_DIR_NAME = join(download_directory, creator_name_to_create_for+'_fansly', 'Timeline') + BASE_DIR_NAME = join(download_directory, creator_name_to_create_for, 'Timeline') else: - BASE_DIR_NAME = join(download_directory, creator_name_to_create_for+'_fansly') # use their custom path & specify new folder for the current creator in it + BASE_DIR_NAME = join(download_directory, creator_name_to_create_for) # use their custom path & specify new folder for the current creator in it # validate BASE_DIR_NAME; if current download folder wasn't created with content separation, disable it for this download session too - correct_File_Hierarchy, tmp_BDR = True, BASE_DIR_NAME.partition('_fansly')[0] + '_fansly' + correct_File_Hierarchy, tmp_BDR = True, BASE_DIR_NAME.partition('_fansly')[0] if os.path.isdir(tmp_BDR): for directory in os.listdir(tmp_BDR): if os.path.isdir(join(tmp_BDR, directory)): @@ -1264,14 +1278,12 @@ def process_folder(folder_path: str): output(2,'\n [25]ERROR','', 'Bad response from fansly API. Please make sure your configuration file is not malformed.') print('\n'+str(e)) print(raw_req.text) - input('\nPress Enter to close ...') - exit() + exit('\nPress Enter to close ...') except IndexError as e: output(2,'\n [26]ERROR','', 'Bad response from fansly API. Please make sure your configuration file is not malformed; most likely misspelled the creator name.') print('\n'+str(e)) print(raw_req.text) - input('\nPress Enter to close ...') - exit() + exit('\nPress Enter to close ...') # below only needed by timeline; but wouldn't work without acc_req so it's here # determine if followed @@ -1291,8 +1303,7 @@ def process_folder(folder_path: str): total_timeline_pictures = acc_req['timelineStats']['imageCount'] except KeyError: output(2,'\n [27]ERROR','', f"Can not get timelineStats for creator username \'{config_username}\'; most likely misspelled it!") - input('\nPress Enter to close ...') - exit() + exit('\nPress Enter to close ...') total_timeline_videos = acc_req['timelineStats']['videoCount'] # overwrite base dup threshold with custom 20% of total timeline content @@ -1461,8 +1472,7 @@ def process_folder(folder_path: str): except Exception: print('\n'+traceback.format_exc()) output(2,'\n [34]ERROR','', 'Please copy & paste this on GitHub > Issues & provide a short explanation.') - input('\nPress Enter to close ...') - exit() + exit('\nPress Enter to close ...') except KeyError: output(2,'\n [35]ERROR','', "Couldn\'t find any scrapable media at all!\ @@ -1510,5 +1520,4 @@ def process_folder(folder_path: str): open_location(BASE_DIR_NAME) -input('\n Press Enter to close ..') -exit() +exit('\n Press Enter to close ..')