diff --git a/Dockerfile b/Dockerfile index 2fb82a5..dd61360 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,10 +7,9 @@ COPY . . RUN apt-get update && apt-get install -y --no-install-recommends \ ca-certificates curl firefox-esr \ && rm -fr /var/lib/apt/lists/* \ - && curl -L https://github.com/mozilla/geckodriver/releases/download/v0.32.0/geckodriver-v0.32.0-linux64.tar.gz | tar xz -C /usr/local/bin \ + && curl -L https://github.com/mozilla/geckodriver/releases/download/v0.32.0/geckodriver-v0.33.0-linux64.tar.gz | tar xz -C /usr/local/bin \ && apt-get purge -y ca-certificates curl -RUN pip install --upgrade pip && pip install build && python -m build -RUN pip install dist/*.whl +RUN pip install . ENTRYPOINT ["facebook_downloader"] diff --git a/facebook_downloader/downloader.py b/facebook_downloader/downloader.py index 282162e..01af498 100644 --- a/facebook_downloader/downloader.py +++ b/facebook_downloader/downloader.py @@ -1,91 +1,144 @@ import os -import argparse import requests +import argparse from tqdm import tqdm from selenium import webdriver from selenium.webdriver.common.by import By from selenium.webdriver.common.keys import Keys from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions - + class FacebookDownloader: def __init__(self): - parser = argparse.ArgumentParser(description='facebook-downloader — by Richard Mwewa') - parser.add_argument('url', help='facebook video url (eg. https://www.facebook.com/PageName/videos/VideoID') + self.__program_version_number = "1.4.0" + self.__base_url = "https://getfvid.com" + self.__update_check_endpoint = "https://api.github.com/repos/rly0nheart/facebook-downloader/releases/latest" + self.__home_directory = os.path.expanduser("~") + self.__downloads_directory = os.path.join(self.__home_directory, "facebook-videos") + + __option = webdriver.FirefoxOptions() + __option.add_argument('--headless') + self.__driver = webdriver.Firefox(options=__option) + + parser = argparse.ArgumentParser(description='facebook-downloader — by Richard Mwewa', + epilog='Facebook video downloader.') + parser.add_argument('url', help='facebook video url') parser.add_argument('-a', '--audio', help='download file as audio', action='store_true') - parser.add_argument('-o', '--output', help='output filename') - self.args = parser.parse_args() + parser.add_argument('-o', '--output', help='output filename', default="") + parser.add_argument('-v', '--version', action='version', version=self.__program_version_number) + self.__args = parser.parse_args() - option = webdriver.FirefoxOptions() - option.add_argument('--headless') - self.driver = webdriver.Firefox(options=option) + @staticmethod + def __format_output_filename(user_defined_name) -> str: + """ + Formats the output file's name. - self.program_version_number = "1.3.1" - self.downloading_url = "https://getfvid.com" - self.update_check_endpoint = "https://api.github.com/repos/rly0nheart/facebook-downloader/releases/latest" - - - def notice(self): + :param user_defined_name: User-defined name for the file. + :return: Formatted/Reconstructed name of the file. + """ + from datetime import datetime + + dt_now = datetime.now() + if os.name == "nt": + output_name = dt_now.strftime(f"{user_defined_name}_%d-%m-%Y %I-%M-%S%p-facebook-downloader.mp4") + else: + output_name = dt_now.strftime(f"{user_defined_name}_%d-%m-%Y %I:%M:%S%p-facebook-downloader.mp4") + + return output_name + + def notice(self) -> str: + """ + Returns the program's license notice and current version. + + :return: License notice. + :rtype: str + """ return f""" - facebook-downloader v{self.program_version_number} Copyright (C) 2023 Richard Mwewa + facebook-downloader v{self.__program_version_number} Copyright (C) 2023 Richard Mwewa This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. """ - - - def check_updates(self): - print(self.notice()) - response = requests.get(self.update_check_endpoint).json() - if response['tag_name'] == self.program_version_number: - """Ignore if the program is up to date""" - pass - else: - print(f"[UPDATE] A new release is available ({response['tag_name']}). Run 'pip install --upgrade facebook-downloader' to get the updates.") - - def download_type(self): + def check_updates(self) -> None: """ - The elements change according to what file type will be downloaded - So, we pass an option to specify what file type we want, by default the file is an HD video + Checks if the program's version tag matches the tag of the latest release on GitHub. + If the tags match, assume the program is up-to-date. + + :return: None """ + with requests.get(self.__update_check_endpoint) as response: + if response.json()['tag_name'] != self.__program_version_number: + print(f"* A new release is available -> facebook-downloader v{response.json()['tag_name']}.\n" + f"* Run 'pip install --upgrade facebook-downloader' to get the updates.\n") + else: + pass + + def __get_download_type_element(self) -> str: """ - HD: "/html/body/div[2]/div/div/div[1]/div/div[2]/div/div[3]/p[1]/a" - SD: "/html/body/div[2]/div/div/div[1]/div/div[2]/div/div[3]/p[2]/a" - Audio: "/html/body/div[2]/div/div/div[1]/div/div[2]/div/div[3]/p[3]/a" + Gets the web element according to the specified command-line arguments. + + HD: /html/body/div[2]/div/div/div[1]/div/div[2]/div/div[3]/p[1]/a + + SD: /html/body/div[2]/div/div/div[1]/div/div[2]/div/div[3]/p[2]/a + + Audio: /html/body/div[2]/div/div/div[1]/div/div[2]/div/div[3]/p[3]/a + + :return: Web element """ - if self.args.audio: + if self.__args.audio: download_type_element = "/html/body/div[2]/div/div/div[1]/div/div[2]/div/div[3]/p[3]/a" else: download_type_element = "/html/body/div[2]/div/div/div[1]/div/div[2]/div/div[3]/p[1]/a" - + return download_type_element + def path_finder(self) -> None: + """ + Creates the facebook-videos directory. + + :return: None + """ + # Construct and create the directory if it doesn't already exist + os.makedirs(os.path.join(self.__home_directory, "facebook-videos"), exist_ok=True) - def path_finder(self): - os.makedirs("downloads", exist_ok=True) - - def download_video(self): - self.path_finder() - self.check_updates() - self.driver.get(self.downloading_url) # Opening getfvid.com, a website that downloads facebook videos - url_entry_field = self.driver.find_element(By.NAME, "url") # Find the url entry field - url_entry_field.send_keys(self.args.url) # write facebook url in the entry field - url_entry_field.send_keys(Keys.ENTER) # press enter - print('[INFO] Loading web resource, please wait...') - # self.driver.refresh - - download_btn = WebDriverWait(self.driver, 20).until(expected_conditions.presence_of_element_located((By.XPATH, self.download_type()))) # Find the download button (this clicks the first button which returns a video in hd) + """ + Opens https://getfvid.com with selenium and uses the specified facebook video link as a query. + + :return: + """ + # Open the base url. + self.__driver.get(self.__base_url) + + # Locate the facebook video url entry field. + url_entry_field = self.__driver.find_element(By.NAME, "url") + + # Write the given facebook video url in the entry field. + url_entry_field.send_keys(self.__args.url) + + # Press ENTER. + url_entry_field.send_keys(Keys.ENTER) + print("* Loading web resources... Please wait.") + + # Find the download button (this clicks the first button which returns a video in hd). + download_btn = WebDriverWait(self.__driver, 20).until( + expected_conditions.presence_of_element_located((By.XPATH, + self.__get_download_type_element()))) + # Get the video download url from the download button. download_url = download_btn.get_attribute('href') - + + # Open the download url and stream the content to a file. with requests.get(download_url, stream=True) as response: response.raise_for_status() - with open(os.path.join('downloads', f'{self.args.output}.mp4'), 'wb') as file: - for chunk in tqdm(response.iter_content(chunk_size=8192), desc=f'[INFO] Downloading: {self.args.output}.mp4'): + with open(os.path.join(self.__downloads_directory, + self.__format_output_filename(self.__args.output)), 'wb') as file: + for chunk in tqdm(response.iter_content(chunk_size=8192), + desc=f"* Downloading: {file.name}"): file.write(chunk) - print(f'[INFO] Downloaded: {file.name}') - self.driver.close() - + print(f"* Downloaded: {file.name}") + + # Close driver. + self.__driver.close() diff --git a/facebook_downloader/main.py b/facebook_downloader/main.py index b2b5322..645d67b 100644 --- a/facebook_downloader/main.py +++ b/facebook_downloader/main.py @@ -1,12 +1,25 @@ from facebook_downloader.downloader import FacebookDownloader -def main(): + +def run(): try: - start = FacebookDownloader() - start.download_video() - + # Initialise the FaceBookDownloader instance. + program = FacebookDownloader() + + # Create directory where downloaded videos will be stored. + program.path_finder() + + # Print program's license notice. + print(program.notice()) + + # Check for latest releases. + program.check_updates() + + # Start video download. + program.download_video() + except KeyboardInterrupt: - print('[WARNING] Process interrupted with Ctrl+C.') + print("Process interrupted with Ctrl+C.") except Exception as e: - print('[ERROR]', e) + print(f"An error occurred: {e}") diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..5e495f6 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,33 @@ +[build-system] +requires = ["setuptools", "setuptools-scm"] +build-backend = "setuptools.build_meta" + +[project] +name = "facebook-downloader" +version = "1.4.0" +description = "A Facebook video downloader." +readme = "README.md" +requires-python = ">=3.9" +license = {file = "LICENSE"} +dependencies = ["tqdm", "requests", "selenium"] +keywords = ["searchcode", "search-engine", "codesearch", "api-wrapper"] +authors = [{name = "Richard Mwewa", email = "rly0nheart@duck.com"}] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Programming Language :: Python :: 3", + "Intended Audience :: Information Technology", + "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", + "Operating System :: OS Independent", + "Natural Language :: English", +] + +[tool.setuptools] +packages = ["facebook_downloader"] + +[project.urls] +homepage = "https://www.bellingcat.com" +documentation = "https://github.com/bellingcat/facebook-downloader/wiki" +repository = "https://github.com/bellingcat/facebook-downloader" + +[project.scripts] +facebook_downloader = "facebook_downloader.main:run" diff --git a/setup.py b/setup.py deleted file mode 100644 index 5addf4a..0000000 --- a/setup.py +++ /dev/null @@ -1,31 +0,0 @@ -import setuptools - -with open('README.md', 'r', encoding='utf-8') as file: - long_description = file.read() - -setuptools.setup( - name='facebook-downloader', - version='1.3.1', - author='Richard Mwewa', - author_email='rly0nheart@duck.com', - packages=['facebook_downloader'], - description='Facebook video downloader', - long_description=long_description, - long_description_content_type='text/markdown', - url='https://github.com/rly0nheart/facebook-downloader', - license='GNU General Public License v3 (GPLv3)', - install_requires=['requests', 'selenium', 'tqdm'], - classifiers=[ - 'Development Status :: 5 - Production/Stable', - 'Intended Audience :: Information Technology', - 'License :: OSI Approved :: GNU General Public License v3 (GPLv3)', - 'Operating System :: OS Independent', - 'Natural Language :: English', - 'Programming Language :: Python :: 3' - ], - entry_points={ - 'console_scripts': [ - 'facebook_downloader=facebook_downloader.main:main', - ] - }, -)