├── src ├── __init__.py ├── helpers │ ├── __init__.py │ ├── cli.py │ ├── video_processing.py │ ├── youtube.py │ └── twitch.py ├── secrets │ ├── twitch_creds_template.json │ └── youtube_creds_template.md ├── config.py └── clip_captain.py ├── thumbnails └── placeholder.txt ├── static ├── beach.jpeg ├── scripts.js └── main.css ├── assets ├── frontend.png └── tvstatictransition.mp4 ├── requirements.txt ├── .gitignore ├── README.md ├── templates ├── process.html └── index.html └── .pylintrc /src/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/helpers/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /thumbnails/placeholder.txt: -------------------------------------------------------------------------------- 1 | allows tracking of thumbnails folder -------------------------------------------------------------------------------- /static/beach.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NikoRaisanen/clip-captain/HEAD/static/beach.jpeg -------------------------------------------------------------------------------- /assets/frontend.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NikoRaisanen/clip-captain/HEAD/assets/frontend.png -------------------------------------------------------------------------------- /assets/tvstatictransition.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NikoRaisanen/clip-captain/HEAD/assets/tvstatictransition.mp4 -------------------------------------------------------------------------------- /src/secrets/twitch_creds_template.json: -------------------------------------------------------------------------------- 1 | { 2 | "client_secret": "xxxxxxxxxxxxxxxxxx", 3 | "client_id": "xxxxxxxxxxxxxxxxxx" 4 | } -------------------------------------------------------------------------------- /src/secrets/youtube_creds_template.md: -------------------------------------------------------------------------------- 1 | Go to this link, download your oauth credentials, and paste the .json file into this directory 2 | 3 | See: https://developers.google.com/identity/protocols/oauth2/web-server#creatingcred -------------------------------------------------------------------------------- /src/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | # Path to youtube api keys 5 | YT_SECRETS_PATH = os.path.join(os.getcwd(), 'src', 'secrets', 'youtube_creds.json') 6 | 7 | # Path to twitch api keys 8 | TWITCH_SECRETS_PATH = os.path.join(os.getcwd(), 'src', 'secrets', 'twitch_creds.json') 9 | 10 | # Path to clip download directory 11 | CLIP_PATH = os.path.join(os.getcwd(), 'clips') 12 | 13 | # Path to default transition media 14 | DEFAULT_TRANSITION_PATH = os.path.join(os.getcwd(), 'assets', 'tvstatictransition.mp4') 15 | 16 | # Path to final video 17 | FINAL_VID_PATH = os.path.join(os.getcwd(), 'finalVideos') 18 | 19 | # Language options of twitch clips 20 | VALID_LANGUAGES = ['en', 'es', 'fr', 'ru', 'ko', 'sv', 'pt', 'da'] 21 | 22 | # Max number of clips to use from a single creator. Set to None to use all clips 23 | CLIPS_PER_CREATOR = 1 24 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | beautifulsoup4==4.9.3 2 | cachetools==4.2.2 3 | certifi==2021.5.30 4 | chardet==4.0.0 5 | click==8.0.1 6 | decorator==4.4.2 7 | Flask==2.0.1 8 | google==3.0.0 9 | google-api-core==1.30.0 10 | google-api-python-client==2.12.0 11 | google-auth==1.32.1 12 | google-auth-httplib2==0.1.0 13 | google-auth-oauthlib==0.4.4 14 | googleapis-common-protos==1.53.0 15 | httplib2==0.19.1 16 | idna==2.10 17 | imageio==2.9.0 18 | imageio-ffmpeg==0.4.4 19 | itsdangerous==2.0.1 20 | Jinja2==3.0.1 21 | MarkupSafe==2.0.1 22 | moviepy==1.0.3 23 | numpy==1.21.0 24 | oauthlib==3.1.1 25 | packaging==21.0 26 | Pillow==8.3.0 27 | proglog==0.1.9 28 | protobuf==3.17.3 29 | pyasn1==0.4.8 30 | pyasn1-modules==0.2.8 31 | pyparsing==2.4.7 32 | pytz==2021.1 33 | requests==2.25.1 34 | requests-oauthlib==1.3.0 35 | rsa==4.7.2 36 | selenium==3.141.0 37 | six==1.16.0 38 | soupsieve==2.2.1 39 | tqdm==4.61.2 40 | uritemplate==3.0.1 41 | urllib3==1.26.6 42 | Werkzeug==2.0.1 43 | -------------------------------------------------------------------------------- /static/scripts.js: -------------------------------------------------------------------------------- 1 | function get_status() { 2 | currentText = document.getElementById('userUpdate').innerText 3 | fetch('/status') 4 | .then(function (response) { 5 | return response.json(); // But parse it as JSON this time 6 | }) 7 | .then(function (json) { 8 | console.log(JSON.stringify(json.status)) 9 | console.log("tHIS IS CURRENT TEXT:" + currentText) 10 | document.getElementById('userUpdate').innerText = JSON.stringify(json.status) 11 | 12 | }) 13 | setTimeout(get_status, 5000); 14 | } 15 | 16 | var slider = document.getElementById("myRange"); 17 | var output = document.getElementById("numClips"); 18 | output.innerHTML = "Number of clips to use: " + String(slider.value) + ""; 19 | 20 | // Dynamically update slider value 21 | slider.oninput = function() { 22 | output.innerHTML = "Number of clips to use: " + String(this.value) + "" 23 | } -------------------------------------------------------------------------------- /src/helpers/cli.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | 3 | 4 | def start(): 5 | """Get params from cli""" 6 | # Params for Twitch api and clip download 7 | parser = argparse.ArgumentParser(description="Automatically create and upload a gaming youtube video") 8 | parser.add_argument("-g", "--game", type=str, help="Name of target game", required=True) 9 | parser.add_argument("-pd", "--past-days", type=int, help="Get clips from the past X days", required=False, default=7) 10 | parser.add_argument("-n", "--num-clips", type=int, help="Number of clips to be used in your video", required=False, default=20) 11 | parser.add_argument("-f", "--first", type=int, help="Fetch X clips on each Twitch api call", required=False, default=50) 12 | parser.add_argument("-l", "--language", type=str, help="Language of fetched clips", required=False, default='en') 13 | 14 | # Params for Youtube Video 15 | parser.add_argument("-vt", "--video-title", type=str, help="Title of your Youtube video", required=True) 16 | parser.add_argument("-tn", "--thumbnail", type=str, help="Path to the thumbnail of your video", required=False, default='') 17 | parser.add_argument("-t", "--tags", nargs='+', help="Tags for your Youtube video", required=False, default=None) 18 | parser.add_argument("-d", "--description", type=str, help="Description for -your Youtube video", required=False, default=None) 19 | parser.add_argument("-p", "--privacy-status", type=str, help="Privacy status for your Youtube Video", required=False, default='private', choices=['unlisted', 'private', 'public']) 20 | parser.add_argument("-tm", "--transition-media", type=str, help="Path to transition media for combining clips", required=False, default=None) 21 | 22 | args = parser.parse_args() 23 | return args 24 | -------------------------------------------------------------------------------- /src/helpers/video_processing.py: -------------------------------------------------------------------------------- 1 | """Module that handles video and text generation""" 2 | import os 3 | from moviepy.editor import * 4 | from config import CLIP_PATH, DEFAULT_TRANSITION_PATH, FINAL_VID_PATH 5 | 6 | 7 | def add_creator_watermark(clips, game_name): 8 | """Add creator watermark to their respective video(s)""" 9 | download_path = os.path.join(CLIP_PATH, game_name) 10 | videos = [] 11 | # set up the composite video that includes streamer name 12 | for clip in clips: 13 | # Add text below: 14 | video = VideoFileClip(os.path.join(download_path, clip.filename), target_resolution=(1080,1920)) 15 | txt_clip = TextClip(clip.streamer_name, fontsize = 60, color = 'white',stroke_color='black',stroke_width=2, font="Fredoka-One") 16 | txt_clip = txt_clip.set_pos((0.8, 0.9), relative=True).set_duration(video.duration) 17 | video = CompositeVideoClip([video, txt_clip]).set_duration(video.duration) 18 | videos.append(video) 19 | return videos 20 | 21 | 22 | def create_video(videos, transition, filename): 23 | """Stitch videos together and save the video to disk""" 24 | if not transition: 25 | transition = DEFAULT_TRANSITION_PATH 26 | 27 | print(f'Using {transition} as the transitioning media') 28 | # Make transition clip 1 second long and halve the volume 29 | transition = VideoFileClip(transition).fx(afx.volumex, 0.5) 30 | transition = transition.subclip(0, -1) 31 | print('Beginning to concatenate video clips...') 32 | final = concatenate_videoclips(videos, transition=transition, method='compose') 33 | 34 | if not os.path.exists(FINAL_VID_PATH): 35 | os.mkdir('finalVideos') 36 | # TODO: explore using more threads to speed up the process 37 | final.write_videofile(os.path.join(FINAL_VID_PATH, filename), fps=60, bitrate="6000k", threads=8) 38 | return os.path.join(FINAL_VID_PATH, filename) 39 | 40 | 41 | def finalize_video(clips, transition, filename, game_name): 42 | """Add watermark to clips, stitch videos together with transition""" 43 | videos = add_creator_watermark(clips, game_name) 44 | return create_video(videos, transition, filename) 45 | -------------------------------------------------------------------------------- /src/helpers/youtube.py: -------------------------------------------------------------------------------- 1 | """Module that handles user consent and youtube api""" 2 | import socket 3 | from moviepy.editor import * 4 | from googleapiclient.http import MediaFileUpload 5 | from googleapiclient.discovery import build 6 | from google_auth_oauthlib.flow import InstalledAppFlow 7 | from config import YT_SECRETS_PATH 8 | 9 | 10 | socket.setdefaulttimeout(100000) 11 | 12 | 13 | def get_authenticated_service(): 14 | """Local oauth flow, returns authenticated youtube service object""" 15 | CLIENT_SECRET_FILE = YT_SECRETS_PATH 16 | API_NAME = 'youtube' 17 | API_VERSION = 'v3' 18 | SCOPES = ['https://www.googleapis.com/auth/youtube.upload'] 19 | # Authenticate on each request for web version 20 | flow = InstalledAppFlow.from_client_secrets_file( 21 | CLIENT_SECRET_FILE, 22 | scopes=SCOPES) 23 | print('Select the google account where you would like to upload the video') 24 | credentials = flow.run_local_server() 25 | 26 | service = build(API_NAME, API_VERSION, credentials=credentials) 27 | return service 28 | 29 | 30 | def upload_video(service, video): 31 | """Takes in authenticated yt service and custom video class, uploads video to youtube""" 32 | if not video.description: 33 | video.set_default_description() 34 | 35 | request_body = { 36 | 'snippet': { 37 | 'title': video.title, 38 | 'categoryId': '20', 39 | 'description': video.description, 40 | 'tags': video.tags, 41 | 'defaultLanguage': video.language, 42 | }, 43 | 'status': { 44 | 'privacyStatus': video.privacy_status, 45 | 'selfDeclaredMadeForKids': False 46 | }, 47 | 'notifySubscribers': False 48 | } 49 | file = MediaFileUpload(video.filename) 50 | print(f'Uploading the following file: {video.filename}') 51 | 52 | 53 | print(f'Uploading video with the following information...\n{request_body}') 54 | response_upload = service.videos().insert( 55 | part = 'snippet,status', 56 | body = request_body, 57 | media_body = file 58 | ).execute() 59 | print(f'response_upload: {response_upload}') 60 | # Set thumbnail if valid file 61 | try: 62 | service.thumbnails().set( 63 | videoId=response_upload.get('id'), 64 | media_body=MediaFileUpload(video.thumbnail) 65 | ).execute() 66 | except FileNotFoundError: 67 | print(f'{video.thumbnail} could not be found, not updating thumbnail...') 68 | 69 | print('Upload complete!') 70 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 98 | __pypackages__/ 99 | 100 | # Celery stuff 101 | celerybeat-schedule 102 | celerybeat.pid 103 | 104 | # SageMath parsed files 105 | *.sage.py 106 | 107 | # Environments 108 | .env 109 | .venv 110 | env/ 111 | venv/ 112 | ENV/ 113 | env.bak/ 114 | venv.bak/ 115 | 116 | # Spyder project settings 117 | .spyderproject 118 | .spyproject 119 | 120 | # Rope project settings 121 | .ropeproject 122 | 123 | # mkdocs documentation 124 | /site 125 | 126 | # mypy 127 | .mypy_cache/ 128 | .dmypy.json 129 | dmypy.json 130 | 131 | # Pyre type checker 132 | .pyre/ 133 | 134 | # pytype static type analyzer 135 | .pytype/ 136 | 137 | # Cython debug symbols 138 | cython_debug/ 139 | 140 | # Private stuff, credentials, etc. 141 | /clips/* 142 | twitch_creds.json 143 | youtube_creds.json -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Automating Youtube Gaming Highlight Channels (Work in Progress) 2 | ![Web front-end of the application](https://github.com/NikoRaisanen/Youtube-Automation/blob/main/assets/frontend.png) 3 | ## Why was this application created? 4 | 5 | On Youtube you can find hundreds of channels that create gaming compilation videos which receive millions of views. With such viewcounts comes the opportunity to monetize through ad revenue and sponsor activations. 6 | Many of these videos are quite basic in nature, the format is as follows: 7 | 8 | 9 | * Get 5+ minutes of gaming footage, highlights, etc. 10 | * Overlay the name of the original content creator over the video 11 | * Stitch highlights together with a common transition 12 | * **Profit $$$** 13 | 14 | 15 | With such a simple formula to create this type of video, there must be a way to automate the entire process... 16 | 17 | ## What this application does 18 | 1. Queries Twitch Clips API for the X most popular clips (videos) for the selected game 19 | 2. Downloads these clips into a directory called clips 20 | 3. A text overlay is added to each clip which contains the the content creator's name 21 | 4. These updated clips are edited together with a common "tv static" transition 22 | 5. The resulting video is written into a directory called finalVideos 23 | 6. The final video is uploaded to YOUR Youtube channel according to the settings specified in the front-end application 24 | 25 | Sample of what the resulting videos look like: 26 | **[link to video PoC]** 27 | 28 | All of the content for this video was automatically scraped, edited, and uploaded by filling out a simple form and authenticating with a Google account. 29 | 30 | ## How it works 31 | The project wasn't as straight-forward as it may seem, in the below video I take you through a deeper dive on how the program works under the hood 32 | **[add link to YT video that I will make]** 33 | 34 | ## Crediting the content owners 35 | This program takes video content from many creators and consolidates it into a single video. Credit to the original creator is given in the following ways: 36 | 37 | * A "watermark" with the creator's name is applied on the bottom right of the video while their clip is being played 38 | * The application's auto-generated description provides a link to the Twitch channels of all creators that appeared in the video 39 | 40 | ### Run this program locally 41 | This program will **not** work if you only clone it. You will need to get API keys for Twitch and Youtube Data API v3. Upon receiving these API keys you can download the respective json files and name them "twitchCreds.json" and "client_secret.json." Put these 2 json files in the root project directory and your API keys will be used to execute the program! 42 | ### Use this program as a SaaS model 43 | I deployed this application onto Google App Engine using a production-quality web server (gunicorn) so that it can be used as a SaaS. 44 | Clicking the "Upload Video" on the GCP-hosted application does not work at the moment. I configured the OAuth Flow to work when the program is accessed locally, the OAuth integration for remote connections is a WIP 45 | -------------------------------------------------------------------------------- /templates/process.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Effortless Youtube Videos 5 | 6 | 7 | 8 | 9 | 10 |
11 |
12 | 15 |
16 |
17 |

Game:  {{ gameName }}

18 |
19 |
20 |

Title:  {{ videoTitle }}

21 |
22 |
23 |

Thumbnail filename:  {{ thumbnail }}

24 |
25 |
26 |

Number of Clips:  {{ numClips }}

27 |
28 |
29 |

Privacy Status:  {{ privacyStatus }}

30 |
31 | 36 |
37 |

Tags:  {{ tags }}

38 |
39 |
40 | 41 |

Description:
42 | {% if session['description'] %} 43 | 44 | {{ description }} 45 | {% else %} 46 | 47 | {{session['videoTitle']}} 48 |
49 |
50 | 51 | Make sure to support the streamers in the video! 52 |
53 |
54 | [**Crediting the owner of each clip (example below)**] 55 |
56 | https://www.twitch.tv/shroud 57 |
58 | https://www.twitch.tv/vanity 59 |
60 | https://www.twitch.tv/ShahZaM 61 |
62 | https://www.twitch.tv/BOOMBURAPA 63 |
64 | https://www.twitch.tv/LeviathanAG_ 65 |
66 | https://www.twitch.tv/LotharHS 67 |
68 | https://www.twitch.tv/Nikolarn 69 | {% endif %}

70 |
71 |
72 |
73 | 77 |
78 |

Current Progress

79 |

80 |
81 |
82 |
83 | 84 |
85 |
86 |
87 | 88 | 89 | -------------------------------------------------------------------------------- /src/clip_captain.py: -------------------------------------------------------------------------------- 1 | #!/bin/python 2 | from moviepy.editor import * 3 | import helpers.twitch as twitch 4 | import helpers.cli as cli 5 | import helpers.youtube as yt 6 | import helpers.video_processing as vid_p 7 | from config import VALID_LANGUAGES 8 | 9 | 10 | # Data structure for the final video, used to populate information in Youtube Upload API 11 | # TODO: add support for multiple languages... 12 | # Language format will be needed for clip downloading and yt video upload 13 | # TODO: Possibly break video class into multiple specialized classes 14 | class Video: 15 | """Object to store data about the final video""" 16 | def __init__(self, game_name, language, title, thumbnail, tags, description, privacy_status, streamers=None, clips=None): 17 | self.game_name = game_name 18 | self.title = title 19 | self.language = language 20 | self.streamers = streamers 21 | self.thumbnail = thumbnail 22 | self.tags = tags 23 | self.description = description 24 | self.privacy_status = privacy_status 25 | self.clips = clips 26 | self.filename = f'{game_name}.mp4' 27 | 28 | def __str__(self): 29 | return f''' 30 | Game: {self.game_name} 31 | Title: {self.title} 32 | Thumbnail: {self.thumbnail} 33 | Tags: {self.tags} 34 | Description: {self.description} 35 | Creators: {self.streamers} 36 | Filename: {self.filename} 37 | ''' 38 | 39 | def get_unique_streamers(self): 40 | """Return list of unique streamers""" 41 | return list(set(self.streamers)) 42 | 43 | def set_default_description(self): 44 | """create default description that credits all content owners""" 45 | credit = '' 46 | for streamer in self.streamers: 47 | link = f'https://www.twitch.tv/{streamer}' 48 | credit = f'{credit}\n{link}' 49 | 50 | self.description = f"""{self.title}\n 51 | Make sure to support the streamers in the video! 52 | {credit}""" 53 | 54 | 55 | def validate_language(language): 56 | """Raise error if entered language is not supported""" 57 | if language not in VALID_LANGUAGES: 58 | raise ValueError( 59 | f'Language {language} is not supported. Please use one of the following: {VALID_LANGUAGES}' 60 | ) 61 | 62 | 63 | # TODO: Create bash script to automatically run the program for a given user 64 | # TODO: Configure a python linter 65 | def main(): 66 | args = cli.start() 67 | validate_language(args.language) 68 | creds = twitch.get_credentials() 69 | 70 | # Go through oauth flow before fetching clips 71 | yt_service = yt.get_authenticated_service() 72 | # TODO: Reduce num args that go into twitch.get_clips 73 | clips = twitch.get_clips(args.language, creds, args.game, args.past_days, args.num_clips, args.first) 74 | creators = twitch.get_creator_names(clips) 75 | # TODO: Reduce num args that go into Video constructor 76 | vid = Video(args.game, args.language, args.video_title, args.thumbnail, args.tags, args.description, args.privacy_status, creators, clips) 77 | print(vid) 78 | 79 | vid_path = vid_p.finalize_video(clips, args.transition_media, vid.filename, args.game) 80 | vid.filename = vid_path 81 | 82 | yt.upload_video(yt_service, vid) 83 | 84 | 85 | if __name__ == "__main__": 86 | # Sample Usage: 87 | # .\src\clip_captain.py -g 'Just Chatting' -n 15 -vt 'Twitch Just Chatting | Best Moments of the Week #1' -t 'just chatting' 'twitch' 'best' 'best of' 'best just chatting' 'twitch just chatting' 'compilation' -p 'public' 88 | main() 89 | -------------------------------------------------------------------------------- /static/main.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | min-height: 100%; 3 | } 4 | body, div, form, input, textarea, p { 5 | padding: 0; 6 | margin: 0; 7 | outline: none; 8 | font-family: Roboto, Arial, sans-serif; 9 | font-size: 14px; 10 | color: #666; 11 | line-height: 22px; 12 | } 13 | h1 { 14 | position: absolute; 15 | margin: 0; 16 | font-size: 32px; 17 | color: #fff; 18 | z-index: 2; 19 | } 20 | .testbox { 21 | display: flex; 22 | justify-content: center; 23 | align-items: center; 24 | height: inherit; 25 | padding: 20px; 26 | 27 | } 28 | form { 29 | width: 80%; 30 | padding: 20px; 31 | border-radius: 6px; 32 | background: #fff; 33 | box-shadow: 0 0 10px 0 #cc0052; 34 | } 35 | .banner { 36 | position: relative; 37 | height: 210px; 38 | background-image: url("/static/beach.jpeg"); background-size: cover; 39 | display: flex; 40 | justify-content: center; 41 | align-items: center; 42 | text-align: center; 43 | /* -moz-box-shadow: 0 0 10px black; 44 | -webkit-box-shadow: 0 0 10px black; */ 45 | box-shadow: 0 0 20px black; 46 | } 47 | .banner::after { 48 | content: ""; 49 | background-color: rgba(0, 0, 0, 0.4); 50 | position: absolute; 51 | width: 100%; 52 | height: 100%; 53 | } 54 | input, textarea { 55 | margin-bottom: 10px; 56 | border: 1px solid #ccc; 57 | border-radius: 3px; 58 | } 59 | input { 60 | width: calc(100% - 10px); 61 | padding: 5px; 62 | } 63 | textarea { 64 | width: calc(100% - 12px); 65 | padding: 5px; 66 | } 67 | .item:hover p, input:hover::placeholder { 68 | color: #cc0052; 69 | } 70 | .item input:hover, .item textarea:hover { 71 | border: 1px solid transparent; 72 | box-shadow: 0 0 6px 0 #cc0052; 73 | color: #cc0052; 74 | } 75 | .item { 76 | position: relative; 77 | margin: 10px 0; 78 | width: 50%; 79 | } 80 | .btn-block { 81 | margin-top: 10px; 82 | text-align: center; 83 | } 84 | button { 85 | width: 150px; 86 | padding: 10px; 87 | border: none; 88 | border-radius: 5px; 89 | background: #cc0052; 90 | font-size: 16px; 91 | color: #fff; 92 | cursor: pointer; 93 | } 94 | button:hover { 95 | background: #ff0066; 96 | } 97 | @media (min-width: 568px) { 98 | .name-item, .contact-item { 99 | } 100 | .contact-item .item { 101 | width: 50%; 102 | } 103 | .name-item input { 104 | width: 50%; 105 | } 106 | .name-item select { 107 | width: 50%; 108 | } 109 | .contact-item input { 110 | width: 50%; 111 | } 112 | } 113 | .vidInfo { 114 | font-size: 1.2rem; 115 | border-color: black; 116 | border: 3px; 117 | } 118 | .extrapad { 119 | padding-bottom: 10px; 120 | padding-top: 10px; 121 | } 122 | #userUpdate { 123 | font-size: 2rem; 124 | line-height: 125%; 125 | } 126 | #bannertext { 127 | font-size: 3rem; 128 | /* -webkit-text-stroke: 1px black; */ 129 | color: white; 130 | text-shadow: 131 | 3px 3px 0 #000, 132 | -1px -1px 0 #000, 133 | 1px -1px 0 #000, 134 | -1px 1px 0 #000, 135 | 1px 1px 0 #000; 136 | } 137 | .required:after { 138 | content:" *"; 139 | color: red; 140 | } 141 | /* #nameHelp, #game1 { 142 | display: inline; 143 | } 144 | #nameHelp { 145 | margin-left: 10%; 146 | } */ 147 | .contact-item { 148 | display: inline; 149 | } 150 | .about { 151 | display: inline; 152 | width: 40%; 153 | margin-right: 5%; 154 | float: right; 155 | } 156 | 157 | .about #aboutHeader { 158 | text-align: center; 159 | } 160 | #how { 161 | font-size: 1rem; 162 | line-height: 150%; 163 | } 164 | 165 | /* BELOW IS FOR SLIDER ON MAIN PAGE */ 166 | .slidecontainer { 167 | width: 50%; /* Width of the outside container */ 168 | } -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Effortless Youtube Videos 5 | 6 | 7 | 8 | 9 | 10 |
11 |
12 | 15 |
16 |

About The Project

17 |

This program allows you to make AND upload a highlight video with the push of a button. No need to spend time finding footage, editing it together, or even going to your Youtube studio. 18 |
19 |
20 | In regards to attribution, all clips contain a text overlay which shows the name of the content creator. A link to the channel of each content creator that appears in the video is also provided with the default description. 21 |
22 |
23 | With this software you could hypothetically mass-manufacture highlight videos across many verticals and monetize through Google ad revenue and sponsor activations 24 |

25 |
26 |
27 | Simply put, here is how it works: 28 |
    29 |
  1. The X most popular clips for the selected game are downloaded
  2. 30 |
  3. Clips are "watermarked" with their respective content creator
  4. 31 |
  5. Clips are then merged together with a quality transition and consolidated into a single .mp4 file
  6. 32 |
  7. This .mp4 file is uploaded to your youtube channel with the selected options
  8. 33 |
34 |

35 |
36 |

Game Name

37 |
38 | 39 |

Choose from any game HERE-- Spell game as shown in directory (case insensitive)

40 |
41 |
42 |
43 |
44 |

Video Title

45 | 46 |
47 |
48 |

Upload Thumbnail

49 | 50 |
51 |

52 |
53 | 54 |
55 |
56 |

Privacy Status

57 |
58 | 63 |
64 |
65 | 70 |
71 |

Tags

72 | 73 |
74 |
75 |

Description

76 | 77 |
78 |
79 |
80 | 81 |
82 | 83 |
84 | 85 | 86 | -------------------------------------------------------------------------------- /src/helpers/twitch.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | import datetime 4 | import requests 5 | from config import TWITCH_SECRETS_PATH, CLIP_PATH, CLIPS_PER_CREATOR 6 | 7 | # Data structure for each individual clip 8 | class Clip: 9 | """Stores data for each twitch clip""" 10 | def __init__(self, download_link, streamer_name, filename): 11 | self.download_link = download_link 12 | self.streamer_name = streamer_name 13 | self.filename = filename 14 | 15 | 16 | def get_credentials(): 17 | """Authenticate with twitch api""" 18 | current_creds = {} 19 | try: 20 | with open(TWITCH_SECRETS_PATH, 'r', encoding='utf-8') as fp: 21 | current_creds = json.load(fp) 22 | except FileNotFoundError: 23 | raise FileNotFoundError(f'Please define client_id and client_secret in {TWITCH_SECRETS_PATH} to authenticate with the Twitch api') 24 | 25 | if not current_creds.get('client_id'): 26 | raise KeyError(f'No client_id property found in {TWITCH_SECRETS_PATH}') 27 | if not current_creds.get('client_secret'): 28 | raise KeyError(f'No client_secret property found in {TWITCH_SECRETS_PATH}') 29 | 30 | # check if bearer token still valid 31 | if current_creds.get('bearer_access_token'): 32 | HEADERS = { 33 | 'Authorization': f'Bearer {current_creds["bearer_access_token"]}' 34 | } 35 | validate_endpoint = 'https://id.twitch.tv/oauth2/validate' 36 | r = requests.get(url=validate_endpoint, headers=HEADERS) 37 | if r.status_code == 200: 38 | return current_creds 39 | 40 | # if oauth token not valid, get a new one 41 | new_oauth_token = get_oauth_token(current_creds) 42 | current_creds['bearer_access_token'] = new_oauth_token 43 | update_bearer(new_oauth_token) 44 | return current_creds 45 | 46 | 47 | def update_bearer(bearer): 48 | """Update bearer token in credentials file""" 49 | print('Generating new bearer access token') 50 | curr = None 51 | with open(TWITCH_SECRETS_PATH, 'r') as fp: 52 | curr = json.load(fp) 53 | # update dict with new bearer 54 | curr['bearer_access_token'] = bearer 55 | 56 | # write obj with new bearer to file 57 | with open(TWITCH_SECRETS_PATH, 'w') as fp: 58 | json.dump(fp=fp, obj=curr) 59 | 60 | def get_oauth_token(creds): 61 | """Get oauth token""" 62 | oauth_endpoint = 'https://id.twitch.tv/oauth2/token' 63 | PARAMS = { 64 | 'client_id': creds['client_id'], 65 | 'client_secret': creds['client_secret'], 66 | 'grant_type': 'client_credentials' 67 | } 68 | r = requests.post(url=oauth_endpoint, params=PARAMS) 69 | return r.json().get('access_token') 70 | 71 | 72 | # TODO: Consider printing a mapping of gameId to gameName in console. 73 | def get_game_id(creds, name): 74 | """Get id from game name for future api calls""" 75 | games_endpoint = 'https://api.twitch.tv/helix/games' 76 | PARAMS = {'name': name} 77 | HEADERS = {'Client-Id': creds['client_id'], 'Authorization': f'Bearer {creds["bearer_access_token"]}'} 78 | r = requests.get(url=games_endpoint, params=PARAMS, headers=HEADERS) 79 | resp = r.json() 80 | 81 | try: 82 | return resp['data'][0]['id'] 83 | except Exception: 84 | raise KeyError(f'Cannot find game id for {name}, try adding or removing spaces') 85 | 86 | 87 | # first param must be <= 50 88 | # TODO: Add support for pagination 89 | def get_clip_info(language, creds=None, game_id=None, past_days=7, num_clips = 20, first = 20, cursor = None): 90 | """ 91 | Returns list of Clip objects that contains the following info for 92 | each video clip: filename, download link, and name of creator 93 | """ 94 | counter = 0 95 | clips = [] 96 | clips_per_creator = {} 97 | 98 | 99 | def process_api_response(resp): 100 | """Helper to process api response and append to list of valid clips""" 101 | for item in resp['data']: 102 | # skip clip if not target language 103 | if item['language'] != language: 104 | continue 105 | # break when enough clips have been found 106 | if len(clips) >= num_clips: 107 | break 108 | 109 | creator = item['broadcaster_name'] 110 | if creator not in clips_per_creator: 111 | clips_per_creator[creator] = 1 112 | else: 113 | clips_per_creator[creator] += 1 114 | 115 | if CLIPS_PER_CREATOR and clips_per_creator.get(creator) > CLIPS_PER_CREATOR: 116 | continue 117 | 118 | filename = f'{creator}{clips_per_creator[creator]}.mp4' 119 | download_link = f'{item["thumbnail_url"].split("-preview-")[0]}.mp4' 120 | clip = Clip(download_link, creator, filename) 121 | clips.append(clip) 122 | 123 | 124 | # TODO: allow a custom date range for clips, not only pastDays 125 | time_now = datetime.datetime.now() 126 | start_date = time_now - datetime.timedelta(past_days) 127 | start_date = start_date.isoformat('T') + 'Z' 128 | 129 | # Keep paginating through twitch clips API until enough valid clips 130 | while len(clips) < num_clips: 131 | counter += 1 132 | print(f'Query #{counter} for valid clips') 133 | clipsAPI = 'https://api.twitch.tv/helix/clips' 134 | PARAMS = {'game_id': game_id, 'started_at': start_date, 'first': first, 'after': cursor} 135 | print(f'PARAMS: {PARAMS}') 136 | HEADERS = {'Client-Id': creds['client_id'], 'Authorization': f'Bearer {creds["bearer_access_token"]}'} 137 | r = requests.get(url=clipsAPI, params=PARAMS, headers=HEADERS) 138 | resp = r.json() 139 | cursor = resp['pagination']['cursor'] 140 | process_api_response(resp) 141 | 142 | print('Done getting clip info...') 143 | return clips 144 | 145 | 146 | def get_creator_names(clips): 147 | """Unpack streamer name from clips list""" 148 | return [x.streamer_name for x in clips] 149 | 150 | 151 | def download_clips(clips, game_name): 152 | """Download clips to disk""" 153 | counter = 0 154 | if not os.path.exists(CLIP_PATH): 155 | os.mkdir(CLIP_PATH) 156 | download_path = os.path.join(CLIP_PATH, game_name) 157 | if not os.path.exists(download_path): 158 | os.mkdir(download_path) 159 | 160 | for clip in clips: 161 | counter += 1 162 | r = requests.get(clip.download_link, allow_redirects=True) 163 | with open(os.path.join(download_path, clip.filename), 'wb') as fp: 164 | fp.write(r.content) 165 | print(f'Downloading clip {counter} of {len(clips)} to {os.path.join(download_path, clip.filename)}') 166 | 167 | 168 | def get_clips(language, creds=None, game_name=None, past_days=7, num_clips=20, first=20): 169 | """Wrapper function to perform all Twitch functionality""" 170 | game_id = get_game_id(creds, game_name) 171 | clips = get_clip_info(language, creds, game_id, past_days, num_clips, first) 172 | download_clips(clips, game_name) 173 | return clips 174 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MAIN] 2 | 3 | # Analyse import fallback blocks. This can be used to support both Python 2 and 4 | # 3 compatible code, which means that the block might have code that exists 5 | # only in one or another interpreter, leading to false positives when analysed. 6 | analyse-fallback-blocks=no 7 | 8 | # Load and enable all available extensions. Use --list-extensions to see a list 9 | # all available extensions. 10 | #enable-all-extensions= 11 | 12 | # In error mode, messages with a category besides ERROR or FATAL are 13 | # suppressed, and no reports are done by default. Error mode is compatible with 14 | # disabling specific errors. 15 | #errors-only= 16 | 17 | # Always return a 0 (non-error) status code, even if lint errors are found. 18 | # This is primarily useful in continuous integration scripts. 19 | #exit-zero= 20 | 21 | # A comma-separated list of package or module names from where C extensions may 22 | # be loaded. Extensions are loading into the active Python interpreter and may 23 | # run arbitrary code. 24 | extension-pkg-allow-list= 25 | 26 | # A comma-separated list of package or module names from where C extensions may 27 | # be loaded. Extensions are loading into the active Python interpreter and may 28 | # run arbitrary code. (This is an alternative name to extension-pkg-allow-list 29 | # for backward compatibility.) 30 | extension-pkg-whitelist= 31 | 32 | # Return non-zero exit code if any of these messages/categories are detected, 33 | # even if score is above --fail-under value. Syntax same as enable. Messages 34 | # specified are enabled, while categories only check already-enabled messages. 35 | fail-on= 36 | 37 | # Specify a score threshold under which the program will exit with error. 38 | fail-under=10 39 | 40 | # Interpret the stdin as a python script, whose filename needs to be passed as 41 | # the module_or_package argument. 42 | #from-stdin= 43 | 44 | # Files or directories to be skipped. They should be base names, not paths. 45 | ignore=CVS 46 | 47 | # Add files or directories matching the regular expressions patterns to the 48 | # ignore-list. The regex matches against paths and can be in Posix or Windows 49 | # format. Because '\' represents the directory delimiter on Windows systems, it 50 | # can't be used as an escape character. 51 | ignore-paths= 52 | 53 | # Files or directories matching the regular expression patterns are skipped. 54 | # The regex matches against base names, not paths. The default value ignores 55 | # Emacs file locks 56 | ignore-patterns=^\.# 57 | 58 | # List of module names for which member attributes should not be checked 59 | # (useful for modules/projects where namespaces are manipulated during runtime 60 | # and thus existing member attributes cannot be deduced by static analysis). It 61 | # supports qualified module names, as well as Unix pattern matching. 62 | ignored-modules= 63 | 64 | # Python code to execute, usually for sys.path manipulation such as 65 | # pygtk.require(). 66 | #init-hook= 67 | 68 | # Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the 69 | # number of processors available to use, and will cap the count on Windows to 70 | # avoid hangs. 71 | jobs=1 72 | 73 | # Control the amount of potential inferred values when inferring a single 74 | # object. This can help the performance when dealing with large functions or 75 | # complex, nested conditions. 76 | limit-inference-results=100 77 | 78 | # List of plugins (as comma separated values of python module names) to load, 79 | # usually to register additional checkers. 80 | load-plugins= 81 | 82 | # Pickle collected data for later comparisons. 83 | persistent=yes 84 | 85 | # Minimum Python version to use for version dependent checks. Will default to 86 | # the version used to run pylint. 87 | py-version=3.11 88 | 89 | # Discover python modules and packages in the file system subtree. 90 | recursive=no 91 | 92 | # When enabled, pylint would attempt to guess common misconfiguration and emit 93 | # user-friendly hints instead of false-positive error messages. 94 | suggestion-mode=yes 95 | 96 | # Allow loading of arbitrary C extensions. Extensions are imported into the 97 | # active Python interpreter and may run arbitrary code. 98 | unsafe-load-any-extension=no 99 | 100 | # In verbose mode, extra non-checker-related info will be displayed. 101 | #verbose= 102 | 103 | 104 | [BASIC] 105 | 106 | # Naming style matching correct argument names. 107 | argument-naming-style=snake_case 108 | 109 | # Regular expression matching correct argument names. Overrides argument- 110 | # naming-style. If left empty, argument names will be checked with the set 111 | # naming style. 112 | #argument-rgx= 113 | 114 | # Naming style matching correct attribute names. 115 | attr-naming-style=snake_case 116 | 117 | # Regular expression matching correct attribute names. Overrides attr-naming- 118 | # style. If left empty, attribute names will be checked with the set naming 119 | # style. 120 | #attr-rgx= 121 | 122 | # Bad variable names which should always be refused, separated by a comma. 123 | bad-names=foo, 124 | bar, 125 | baz, 126 | toto, 127 | tutu, 128 | tata 129 | 130 | # Bad variable names regexes, separated by a comma. If names match any regex, 131 | # they will always be refused 132 | bad-names-rgxs= 133 | 134 | # Naming style matching correct class attribute names. 135 | class-attribute-naming-style=any 136 | 137 | # Regular expression matching correct class attribute names. Overrides class- 138 | # attribute-naming-style. If left empty, class attribute names will be checked 139 | # with the set naming style. 140 | #class-attribute-rgx= 141 | 142 | # Naming style matching correct class constant names. 143 | class-const-naming-style=UPPER_CASE 144 | 145 | # Regular expression matching correct class constant names. Overrides class- 146 | # const-naming-style. If left empty, class constant names will be checked with 147 | # the set naming style. 148 | #class-const-rgx= 149 | 150 | # Naming style matching correct class names. 151 | class-naming-style=PascalCase 152 | 153 | # Regular expression matching correct class names. Overrides class-naming- 154 | # style. If left empty, class names will be checked with the set naming style. 155 | #class-rgx= 156 | 157 | # Naming style matching correct constant names. 158 | const-naming-style=UPPER_CASE 159 | 160 | # Regular expression matching correct constant names. Overrides const-naming- 161 | # style. If left empty, constant names will be checked with the set naming 162 | # style. 163 | #const-rgx= 164 | 165 | # Minimum line length for functions/classes that require docstrings, shorter 166 | # ones are exempt. 167 | docstring-min-length=-1 168 | 169 | # Naming style matching correct function names. 170 | function-naming-style=snake_case 171 | 172 | # Regular expression matching correct function names. Overrides function- 173 | # naming-style. If left empty, function names will be checked with the set 174 | # naming style. 175 | #function-rgx= 176 | 177 | # Good variable names which should always be accepted, separated by a comma. 178 | good-names=i, 179 | j, 180 | k, 181 | ex, 182 | Run, 183 | _ 184 | 185 | # Good variable names regexes, separated by a comma. If names match any regex, 186 | # they will always be accepted 187 | good-names-rgxs= 188 | 189 | # Include a hint for the correct naming format with invalid-name. 190 | include-naming-hint=no 191 | 192 | # Naming style matching correct inline iteration names. 193 | inlinevar-naming-style=any 194 | 195 | # Regular expression matching correct inline iteration names. Overrides 196 | # inlinevar-naming-style. If left empty, inline iteration names will be checked 197 | # with the set naming style. 198 | #inlinevar-rgx= 199 | 200 | # Naming style matching correct method names. 201 | method-naming-style=snake_case 202 | 203 | # Regular expression matching correct method names. Overrides method-naming- 204 | # style. If left empty, method names will be checked with the set naming style. 205 | #method-rgx= 206 | 207 | # Naming style matching correct module names. 208 | module-naming-style=snake_case 209 | 210 | # Regular expression matching correct module names. Overrides module-naming- 211 | # style. If left empty, module names will be checked with the set naming style. 212 | #module-rgx= 213 | 214 | # Colon-delimited sets of names that determine each other's naming style when 215 | # the name regexes allow several styles. 216 | name-group= 217 | 218 | # Regular expression which should only match function or class names that do 219 | # not require a docstring. 220 | no-docstring-rgx=^_ 221 | 222 | # List of decorators that produce properties, such as abc.abstractproperty. Add 223 | # to this list to register other decorators that produce valid properties. 224 | # These decorators are taken in consideration only for invalid-name. 225 | property-classes=abc.abstractproperty 226 | 227 | # Regular expression matching correct type variable names. If left empty, type 228 | # variable names will be checked with the set naming style. 229 | #typevar-rgx= 230 | 231 | # Naming style matching correct variable names. 232 | variable-naming-style=snake_case 233 | 234 | # Regular expression matching correct variable names. Overrides variable- 235 | # naming-style. If left empty, variable names will be checked with the set 236 | # naming style. 237 | #variable-rgx= 238 | 239 | 240 | [CLASSES] 241 | 242 | # Warn about protected attribute access inside special methods 243 | check-protected-access-in-special-methods=no 244 | 245 | # List of method names used to declare (i.e. assign) instance attributes. 246 | defining-attr-methods=__init__, 247 | __new__, 248 | setUp, 249 | __post_init__ 250 | 251 | # List of member names, which should be excluded from the protected access 252 | # warning. 253 | exclude-protected=_asdict, 254 | _fields, 255 | _replace, 256 | _source, 257 | _make 258 | 259 | # List of valid names for the first argument in a class method. 260 | valid-classmethod-first-arg=cls 261 | 262 | # List of valid names for the first argument in a metaclass class method. 263 | valid-metaclass-classmethod-first-arg=cls 264 | 265 | 266 | [DESIGN] 267 | 268 | # List of regular expressions of class ancestor names to ignore when counting 269 | # public methods (see R0903) 270 | exclude-too-few-public-methods= 271 | 272 | # List of qualified class names to ignore when counting class parents (see 273 | # R0901) 274 | ignored-parents= 275 | 276 | # Maximum number of arguments for function / method. 277 | max-args=5 278 | 279 | # Maximum number of attributes for a class (see R0902). 280 | max-attributes=7 281 | 282 | # Maximum number of boolean expressions in an if statement (see R0916). 283 | max-bool-expr=5 284 | 285 | # Maximum number of branch for function / method body. 286 | max-branches=12 287 | 288 | # Maximum number of locals for function / method body. 289 | max-locals=15 290 | 291 | # Maximum number of parents for a class (see R0901). 292 | max-parents=7 293 | 294 | # Maximum number of public methods for a class (see R0904). 295 | max-public-methods=20 296 | 297 | # Maximum number of return / yield for function / method body. 298 | max-returns=6 299 | 300 | # Maximum number of statements in function / method body. 301 | max-statements=50 302 | 303 | # Minimum number of public methods for a class (see R0903). 304 | min-public-methods=2 305 | 306 | 307 | [EXCEPTIONS] 308 | 309 | # Exceptions that will emit a warning when caught. 310 | overgeneral-exceptions=BaseException, 311 | Exception 312 | 313 | 314 | [FORMAT] 315 | 316 | # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. 317 | expected-line-ending-format= 318 | 319 | # Regexp for a line that is allowed to be longer than the limit. 320 | ignore-long-lines=^\s*(# )??$ 321 | 322 | # Number of spaces of indent required inside a hanging or continued line. 323 | indent-after-paren=4 324 | 325 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 326 | # tab). 327 | indent-string=' ' 328 | 329 | # Maximum number of characters on a single line. 330 | max-line-length=100 331 | 332 | # Maximum number of lines in a module. 333 | max-module-lines=1000 334 | 335 | # Allow the body of a class to be on the same line as the declaration if body 336 | # contains single statement. 337 | single-line-class-stmt=no 338 | 339 | # Allow the body of an if to be on the same line as the test if there is no 340 | # else. 341 | single-line-if-stmt=no 342 | 343 | 344 | [IMPORTS] 345 | 346 | # List of modules that can be imported at any level, not just the top level 347 | # one. 348 | allow-any-import-level= 349 | 350 | # Allow wildcard imports from modules that define __all__. 351 | allow-wildcard-with-all=no 352 | 353 | # Deprecated modules which should not be used, separated by a comma. 354 | deprecated-modules= 355 | 356 | # Output a graph (.gv or any supported image format) of external dependencies 357 | # to the given file (report RP0402 must not be disabled). 358 | ext-import-graph= 359 | 360 | # Output a graph (.gv or any supported image format) of all (i.e. internal and 361 | # external) dependencies to the given file (report RP0402 must not be 362 | # disabled). 363 | import-graph= 364 | 365 | # Output a graph (.gv or any supported image format) of internal dependencies 366 | # to the given file (report RP0402 must not be disabled). 367 | int-import-graph= 368 | 369 | # Force import order to recognize a module as part of the standard 370 | # compatibility libraries. 371 | known-standard-library= 372 | 373 | # Force import order to recognize a module as part of a third party library. 374 | known-third-party=enchant 375 | 376 | # Couples of modules and preferred modules, separated by a comma. 377 | preferred-modules= 378 | 379 | 380 | [LOGGING] 381 | 382 | # The type of string formatting that logging methods do. `old` means using % 383 | # formatting, `new` is for `{}` formatting. 384 | logging-format-style=old 385 | 386 | # Logging modules to check that the string format arguments are in logging 387 | # function parameter format. 388 | logging-modules=logging 389 | 390 | 391 | [MESSAGES CONTROL] 392 | 393 | # Only show warnings with the listed confidence levels. Leave empty to show 394 | # all. Valid levels: HIGH, CONTROL_FLOW, INFERENCE, INFERENCE_FAILURE, 395 | # UNDEFINED. 396 | confidence=HIGH, 397 | CONTROL_FLOW, 398 | INFERENCE, 399 | INFERENCE_FAILURE, 400 | UNDEFINED 401 | 402 | # Disable the message, report, category or checker with the given id(s). You 403 | # can either give multiple identifiers separated by comma (,) or put this 404 | # option multiple times (only on the command line, not in the configuration 405 | # file where it should appear only once). You can also use "--disable=all" to 406 | # disable everything first and then re-enable specific checks. For example, if 407 | # you want to run only the similarities checker, you can use "--disable=all 408 | # --enable=similarities". If you want to run only the classes checker, but have 409 | # no Warning level messages displayed, use "--disable=all --enable=classes 410 | # --disable=W". 411 | disable=raw-checker-failed, 412 | bad-inline-option, 413 | locally-disabled, 414 | file-ignored, 415 | suppressed-message, 416 | useless-suppression, 417 | deprecated-pragma, 418 | use-symbolic-message-instead 419 | 420 | # Enable the message, report, category or checker with the given id(s). You can 421 | # either give multiple identifier separated by comma (,) or put this option 422 | # multiple time (only on the command line, not in the configuration file where 423 | # it should appear only once). See also the "--disable" option for examples. 424 | enable=c-extension-no-member 425 | 426 | 427 | [METHOD_ARGS] 428 | 429 | # List of qualified names (i.e., library.method) which require a timeout 430 | # parameter e.g. 'requests.api.get,requests.api.post' 431 | timeout-methods=requests.api.delete,requests.api.get,requests.api.head,requests.api.options,requests.api.patch,requests.api.post,requests.api.put,requests.api.request 432 | 433 | 434 | [MISCELLANEOUS] 435 | 436 | # List of note tags to take in consideration, separated by a comma. 437 | notes=FIXME, 438 | XXX, 439 | TODO 440 | 441 | # Regular expression of note tags to take in consideration. 442 | notes-rgx= 443 | 444 | 445 | [REFACTORING] 446 | 447 | # Maximum number of nested blocks for function / method body 448 | max-nested-blocks=5 449 | 450 | # Complete name of functions that never returns. When checking for 451 | # inconsistent-return-statements if a never returning function is called then 452 | # it will be considered as an explicit return statement and no message will be 453 | # printed. 454 | never-returning-functions=sys.exit,argparse.parse_error 455 | 456 | 457 | [REPORTS] 458 | 459 | # Python expression which should return a score less than or equal to 10. You 460 | # have access to the variables 'fatal', 'error', 'warning', 'refactor', 461 | # 'convention', and 'info' which contain the number of messages in each 462 | # category, as well as 'statement' which is the total number of statements 463 | # analyzed. This score is used by the global evaluation report (RP0004). 464 | evaluation=max(0, 0 if fatal else 10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)) 465 | 466 | # Template used to display messages. This is a python new-style format string 467 | # used to format the message information. See doc for all details. 468 | msg-template= 469 | 470 | # Set the output format. Available formats are text, parseable, colorized, json 471 | # and msvs (visual studio). You can also give a reporter class, e.g. 472 | # mypackage.mymodule.MyReporterClass. 473 | #output-format= 474 | 475 | # Tells whether to display a full report or only the messages. 476 | reports=no 477 | 478 | # Activate the evaluation score. 479 | score=yes 480 | 481 | 482 | [SIMILARITIES] 483 | 484 | # Comments are removed from the similarity computation 485 | ignore-comments=yes 486 | 487 | # Docstrings are removed from the similarity computation 488 | ignore-docstrings=yes 489 | 490 | # Imports are removed from the similarity computation 491 | ignore-imports=yes 492 | 493 | # Signatures are removed from the similarity computation 494 | ignore-signatures=yes 495 | 496 | # Minimum lines number of a similarity. 497 | min-similarity-lines=4 498 | 499 | 500 | [SPELLING] 501 | 502 | # Limits count of emitted suggestions for spelling mistakes. 503 | max-spelling-suggestions=4 504 | 505 | # Spelling dictionary name. Available dictionaries: none. To make it work, 506 | # install the 'python-enchant' package. 507 | spelling-dict= 508 | 509 | # List of comma separated words that should be considered directives if they 510 | # appear at the beginning of a comment and should not be checked. 511 | spelling-ignore-comment-directives=fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy: 512 | 513 | # List of comma separated words that should not be checked. 514 | spelling-ignore-words= 515 | 516 | # A path to a file that contains the private dictionary; one word per line. 517 | spelling-private-dict-file= 518 | 519 | # Tells whether to store unknown words to the private dictionary (see the 520 | # --spelling-private-dict-file option) instead of raising a message. 521 | spelling-store-unknown-words=no 522 | 523 | 524 | [STRING] 525 | 526 | # This flag controls whether inconsistent-quotes generates a warning when the 527 | # character used as a quote delimiter is used inconsistently within a module. 528 | check-quote-consistency=no 529 | 530 | # This flag controls whether the implicit-str-concat should generate a warning 531 | # on implicit string concatenation in sequences defined over several lines. 532 | check-str-concat-over-line-jumps=no 533 | 534 | 535 | [TYPECHECK] 536 | 537 | # List of decorators that produce context managers, such as 538 | # contextlib.contextmanager. Add to this list to register other decorators that 539 | # produce valid context managers. 540 | contextmanager-decorators=contextlib.contextmanager 541 | 542 | # List of members which are set dynamically and missed by pylint inference 543 | # system, and so shouldn't trigger E1101 when accessed. Python regular 544 | # expressions are accepted. 545 | generated-members= 546 | 547 | # Tells whether to warn about missing members when the owner of the attribute 548 | # is inferred to be None. 549 | ignore-none=yes 550 | 551 | # This flag controls whether pylint should warn about no-member and similar 552 | # checks whenever an opaque object is returned when inferring. The inference 553 | # can return multiple potential results while evaluating a Python object, but 554 | # some branches might not be evaluated, which results in partial inference. In 555 | # that case, it might be useful to still emit no-member and other checks for 556 | # the rest of the inferred objects. 557 | ignore-on-opaque-inference=yes 558 | 559 | # List of symbolic message names to ignore for Mixin members. 560 | ignored-checks-for-mixins=no-member, 561 | not-async-context-manager, 562 | not-context-manager, 563 | attribute-defined-outside-init 564 | 565 | # List of class names for which member attributes should not be checked (useful 566 | # for classes with dynamically set attributes). This supports the use of 567 | # qualified names. 568 | ignored-classes=optparse.Values,thread._local,_thread._local,argparse.Namespace 569 | 570 | # Show a hint with possible names when a member name was not found. The aspect 571 | # of finding the hint is based on edit distance. 572 | missing-member-hint=yes 573 | 574 | # The minimum edit distance a name should have in order to be considered a 575 | # similar match for a missing member name. 576 | missing-member-hint-distance=1 577 | 578 | # The total number of similar names that should be taken in consideration when 579 | # showing a hint for a missing member. 580 | missing-member-max-choices=1 581 | 582 | # Regex pattern to define which classes are considered mixins. 583 | mixin-class-rgx=.*[Mm]ixin 584 | 585 | # List of decorators that change the signature of a decorated function. 586 | signature-mutators= 587 | 588 | 589 | [VARIABLES] 590 | 591 | # List of additional names supposed to be defined in builtins. Remember that 592 | # you should avoid defining new builtins when possible. 593 | additional-builtins= 594 | 595 | # Tells whether unused global variables should be treated as a violation. 596 | allow-global-unused-variables=yes 597 | 598 | # List of names allowed to shadow builtins 599 | allowed-redefined-builtins= 600 | 601 | # List of strings which can identify a callback function by name. A callback 602 | # name must start or end with one of those strings. 603 | callbacks=cb_, 604 | _cb 605 | 606 | # A regular expression matching the name of dummy variables (i.e. expected to 607 | # not be used). 608 | dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ 609 | 610 | # Argument names that match this expression will be ignored. 611 | ignored-argument-names=_.*|^ignored_|^unused_ 612 | 613 | # Tells whether we should check for unused import in __init__ files. 614 | init-import=no 615 | 616 | # List of qualified module names which can have objects that can redefine 617 | # builtins. 618 | redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io 619 | --------------------------------------------------------------------------------