tcd2 3.2.2.post1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2022 Petter Kraabøl
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,3 @@
1
+ include LICENSE
2
+ include README.md
3
+ recursive-include tcd *.json
@@ -0,0 +1,57 @@
1
+ Metadata-Version: 2.4
2
+ Name: tcd2
3
+ Version: 3.2.2.post1
4
+ Summary: Twitch Chat Downloader (unofficial fork)
5
+ Author: imeloben23
6
+ Maintainer: imeloben23
7
+ License-Expression: MIT
8
+ Project-URL: Homepage, https://github.com/PetterKraabol/Twitch-Chat-Downloader
9
+ Project-URL: Original Project, https://github.com/PetterKraabol/Twitch-Chat-Downloader
10
+ Keywords: Twitch
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: End Users/Desktop
13
+ Classifier: Natural Language :: English
14
+ Classifier: Programming Language :: Python :: 3.8
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Requires-Python: >=3.8
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Requires-Dist: requests==2.27.1
23
+ Requires-Dist: twitch-python==0.0.20
24
+ Requires-Dist: pytz==2022.1
25
+ Requires-Dist: python-dateutil==2.8.2
26
+ Dynamic: license-file
27
+
28
+ # tcd2
29
+
30
+ Unofficial fork of **Twitch Chat Downloader** (the `tcd` package) by Petter Kraabøl.
31
+ This distribution keeps the CLI name `tcd` but publishes under a new name (`tcd2`) on PyPI.
32
+
33
+ ## Install
34
+
35
+ ```bash
36
+ pip install tcd2
37
+ ```
38
+
39
+ ## Usage
40
+
41
+ ```bash
42
+ tcd
43
+ ```
44
+
45
+ ```bash
46
+ # Download chat from VODs by video id
47
+ tcd --video 789654123,987456321 --format irc --output ~/Downloads
48
+ ```
49
+
50
+ ```bash
51
+ # Download chat from the first 10 VODs from multiple streamers
52
+ tcd --channel sodapoppin,nymn,lirik --first=10
53
+ ```
54
+
55
+ ## Credits
56
+
57
+ Original project: https://github.com/PetterKraabol/Twitch-Chat-Downloader
@@ -0,0 +1,30 @@
1
+ # tcd2
2
+
3
+ Unofficial fork of **Twitch Chat Downloader** (the `tcd` package) by Petter Kraabøl.
4
+ This distribution keeps the CLI name `tcd` but publishes under a new name (`tcd2`) on PyPI.
5
+
6
+ ## Install
7
+
8
+ ```bash
9
+ pip install tcd2
10
+ ```
11
+
12
+ ## Usage
13
+
14
+ ```bash
15
+ tcd
16
+ ```
17
+
18
+ ```bash
19
+ # Download chat from VODs by video id
20
+ tcd --video 789654123,987456321 --format irc --output ~/Downloads
21
+ ```
22
+
23
+ ```bash
24
+ # Download chat from the first 10 VODs from multiple streamers
25
+ tcd --channel sodapoppin,nymn,lirik --first=10
26
+ ```
27
+
28
+ ## Credits
29
+
30
+ Original project: https://github.com/PetterKraabol/Twitch-Chat-Downloader
@@ -0,0 +1,52 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "tcd2"
7
+ version = "3.2.2.post1"
8
+ description = "Twitch Chat Downloader (unofficial fork)"
9
+ readme = "README.md"
10
+ requires-python = ">=3.8"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ authors = [
14
+ { name = "imeloben23" }
15
+ ]
16
+ maintainers = [
17
+ { name = "imeloben23" }
18
+ ]
19
+ keywords = ["Twitch"]
20
+ classifiers = [
21
+ "Development Status :: 4 - Beta",
22
+ "Intended Audience :: End Users/Desktop",
23
+ "Natural Language :: English",
24
+ "Programming Language :: Python :: 3.8",
25
+ "Programming Language :: Python :: 3.9",
26
+ "Programming Language :: Python :: 3.10",
27
+ "Programming Language :: Python :: 3.11",
28
+ "Programming Language :: Python :: 3.12"
29
+ ]
30
+ dependencies = [
31
+ "requests==2.27.1",
32
+ "twitch-python==0.0.20",
33
+ "pytz==2022.1",
34
+ "python-dateutil==2.8.2"
35
+ ]
36
+
37
+ [project.urls]
38
+ Homepage = "https://github.com/PetterKraabol/Twitch-Chat-Downloader"
39
+ "Original Project" = "https://github.com/PetterKraabol/Twitch-Chat-Downloader"
40
+
41
+ [project.scripts]
42
+ tcd = "tcd:main"
43
+
44
+ [tool.setuptools]
45
+ include-package-data = true
46
+
47
+ [tool.setuptools.packages.find]
48
+ where = ["."]
49
+ include = ["tcd*"]
50
+
51
+ [tool.setuptools.package-data]
52
+ tcd = ["settings.reference.json"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,96 @@
1
+ import argparse
2
+ import os
3
+ from pathlib import Path
4
+ from typing import List, Callable
5
+
6
+ import requests
7
+
8
+ from .arguments import Arguments
9
+ from .downloader import Downloader
10
+ from .logger import Logger, Log
11
+ from .settings import Settings
12
+
13
+ __name__: str = 'tcd'
14
+ __version__: str = '3.2.2.post1'
15
+ __all__: List[Callable] = [Arguments, Settings, Downloader, Logger, Log]
16
+
17
+
18
+ def main():
19
+ # Arguments
20
+ parser = argparse.ArgumentParser(description=f'Twitch Chat Downloader {__version__}')
21
+ parser.add_argument('-v', f'--{Arguments.Name.VIDEO}', type=str, help='Video IDs separated by commas')
22
+ parser.add_argument('-c', f'--{Arguments.Name.CHANNEL}', type=str, help='Channel names separated by commas')
23
+ parser.add_argument('-u', f'--{Arguments.Name.USER}', type=str, help='Messages from users, separated by commas')
24
+ parser.add_argument(f'--{Arguments.Name.FIRST}', type=int, default=None, help='Download chat from the last n VODs (default: all)')
25
+ parser.add_argument(f'--{Arguments.Name.CLIENT_ID.replace("_", "-")}', type=str, help='Twitch client ID')
26
+ parser.add_argument(f'--{Arguments.Name.CLIENT_SECRET.replace("_", "-")}', type=str, help='Twitch client secret')
27
+ parser.add_argument(f'--{Arguments.Name.VERBOSE}', action='store_true', help='Verbose output')
28
+ parser.add_argument('-q', f'--{Arguments.Name.QUIET}', action='store_true')
29
+ parser.add_argument('-o', f'--{Arguments.Name.OUTPUT}', type=str, help='Output directory', default='./')
30
+ parser.add_argument('-f', f'--{Arguments.Name.FORMAT}', type=str, help='Message format', default='default')
31
+ parser.add_argument(f'--{Arguments.Name.TIMEZONE}', type=str, help='Timezone name')
32
+ parser.add_argument(f'--includes', type=str, help='Messages must include specified text')
33
+ parser.add_argument(f'--{Arguments.Name.INIT}', action='store_true', help='Script setup')
34
+ parser.add_argument(f'--{Arguments.Name.VERSION}', action='store_true', help='Settings version')
35
+ parser.add_argument(f'--{Arguments.Name.FORMATS}', action='store_true', help='List available formats')
36
+ parser.add_argument(f'--{Arguments.Name.PREVIEW}', action='store_true', help='Preview output')
37
+ parser.add_argument(f'--{Arguments.Name.SETTINGS}', action='store_true', help='Print settings file location')
38
+ parser.add_argument(f'--{Arguments.Name.SETTINGS_FILE.replace("_", "-")}', type=str,
39
+ default=str(Path.home()) + '/.config/tcd/settings.json',
40
+ help='Use a custom settings file')
41
+ parser.add_argument(f'--{Arguments.Name.DEBUG}', action='store_true', help='Print debug messages')
42
+ parser.add_argument(f'--{Arguments.Name.LOG}', action='store_true', help='Save log file')
43
+
44
+ Arguments(parser.parse_args().__dict__)
45
+ Settings(Arguments().settings_file,
46
+ reference_filepath=f'{os.path.dirname(os.path.abspath(__file__))}/settings.reference.json')
47
+
48
+ # Print version number
49
+ if Arguments().print_version:
50
+ Logger().log(f'Twitch Chat Downloader {__version__}', retain=False)
51
+ return
52
+
53
+ # Print settings file location
54
+ if Arguments().settings:
55
+ Logger().log(str(Settings().filepath))
56
+ return
57
+
58
+ # Client ID
59
+ Settings().config['client_id'] = Arguments().client_id or Settings().config.get('client_id', None) or input(
60
+ 'Twitch client ID: ').strip()
61
+ Settings().config['client_secret'] = Arguments().client_secret or Settings().config.get('client_secret', None) or input(
62
+ 'Twitch client secret: ').strip()
63
+ Settings().save()
64
+
65
+ Arguments().oauth_token = requests.post(f"https://id.twitch.tv/oauth2/token"
66
+ f"?client_id={Settings().config['client_id']}"
67
+ f"&client_secret={Settings().config['client_secret']}"
68
+ f"&grant_type=client_credentials").json()['access_token']
69
+
70
+ # List formats
71
+ if Arguments().print_formats:
72
+ for format_name in [f for f in Settings().config['formats'] if f not in ['all']]:
73
+ format_dictionary = Settings().config['formats'][format_name]
74
+ Logger().log(f'[{format_name}]', retain=False)
75
+
76
+ if 'comments' in format_dictionary:
77
+ print('comment: {}'.format(Settings().config['formats'][format_name]['comments']['format']))
78
+
79
+ if 'output' in format_dictionary:
80
+ print('output: {}'.format(Settings().config['formats'][format_name]['output']['format']))
81
+
82
+ Logger().log('\n', retain=False)
83
+ return
84
+
85
+ # Downloader
86
+ if Arguments().video_ids or Arguments().channels:
87
+
88
+ if Arguments().video_ids:
89
+ Downloader().videos(Arguments().video_ids)
90
+
91
+ if Arguments().channels:
92
+ Downloader().channels(Arguments().channels)
93
+
94
+ return
95
+
96
+ parser.print_help()
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env python3
2
+
3
+ from . import main
4
+
5
+ if __name__ == "__main__":
6
+ main()
@@ -0,0 +1,84 @@
1
+ from typing import Optional, Dict, Union, List
2
+
3
+ from .singleton import Singleton
4
+
5
+
6
+ class Arguments(metaclass=Singleton):
7
+ """
8
+ Arguments singleton
9
+ """
10
+
11
+ class Name:
12
+ SETTINGS_FILE: str = 'settings_file'
13
+ SETTINGS: str = 'settings'
14
+ INIT: str = 'init'
15
+ VERBOSE: str = 'verbose'
16
+ QUIET: str = 'quiet'
17
+ PREVIEW: str = 'preview'
18
+ FORMATS: str = 'formats'
19
+ VERSION: str = 'version'
20
+ OUTPUT: str = 'output'
21
+ CLIENT_ID: str = 'client_id'
22
+ CLIENT_SECRET: str = 'client_secret'
23
+ CHANNEL: str = 'channel'
24
+ USER: str = 'user'
25
+ INCLUDES: str = 'includes'
26
+ FIRST: str = 'first'
27
+ VIDEO: str = 'video'
28
+ FORMAT: str = 'format'
29
+ TIMEZONE: str = 'timezone'
30
+ DEBUG: str = 'debug'
31
+ LOG: str = 'log'
32
+
33
+ def __init__(self, arguments: Optional[Dict[str, Union[str, bool, int]]] = None):
34
+ """
35
+ Initialize arguments
36
+ :param arguments: Arguments from cli (Optional to call singleton instance without parameters)
37
+ """
38
+
39
+ if arguments is None:
40
+ print('Error: arguments were not provided')
41
+ exit()
42
+
43
+ # Required arguments and booleans
44
+ self.settings_file: str = arguments[Arguments.Name.SETTINGS_FILE]
45
+ self.settings: str = arguments[Arguments.Name.SETTINGS]
46
+ self.init: bool = arguments[Arguments.Name.INIT]
47
+ self.verbose: bool = arguments[Arguments.Name.VERBOSE]
48
+ self.debug: bool = arguments[Arguments.Name.DEBUG]
49
+ self.quiet: bool = arguments[Arguments.Name.QUIET]
50
+ self.preview: bool = arguments[Arguments.Name.PREVIEW]
51
+ self.print_formats: bool = arguments[Arguments.Name.FORMATS]
52
+ self.print_version: bool = arguments[Arguments.Name.VERSION]
53
+ self.output: str = arguments[Arguments.Name.OUTPUT]
54
+ self.log: bool = arguments[Arguments.Name.LOG]
55
+
56
+ # Optional or prompted arguments
57
+ self.client_id: Optional[str] = arguments[Arguments.Name.CLIENT_ID]
58
+ self.client_secret: Optional[str] = arguments[Arguments.Name.CLIENT_SECRET]
59
+ self.oauth_token: Optional[str] = None
60
+ self.first: Optional[int] = arguments[Arguments.Name.FIRST]
61
+ self.timezone: Optional[str] = arguments[Arguments.Name.TIMEZONE]
62
+ self.includes: Optional[str] = arguments[Arguments.Name.INCLUDES]
63
+
64
+ # Arguments that require some formatting
65
+ self.video_ids: List[int] = []
66
+ self.formats: List[str] = []
67
+ self.channels: List[str] = []
68
+ self.users: List[str] = []
69
+
70
+ # Videos
71
+ if arguments[Arguments.Name.VIDEO]:
72
+ self.video_ids = [int(video_id) for video_id in arguments[Arguments.Name.VIDEO].lower().split(',')]
73
+
74
+ # Formats
75
+ if arguments[Arguments.Name.FORMAT]:
76
+ self.formats: Optional[List[str]] = arguments[Arguments.Name.FORMAT].lower().split(',')
77
+
78
+ # Channels
79
+ if arguments[Arguments.Name.CHANNEL]:
80
+ self.channels = arguments[Arguments.Name.CHANNEL].lower().split(',')
81
+
82
+ # Users
83
+ if arguments[Arguments.Name.USER]:
84
+ self.users = arguments[Arguments.Name.USER].lower().split(',')
@@ -0,0 +1,229 @@
1
+ import datetime
2
+ import json
3
+ import os
4
+ import re
5
+ import sys
6
+ from typing import List
7
+
8
+ import dateutil
9
+ from twitch import Helix
10
+ from twitch.helix import Video
11
+
12
+ from .arguments import Arguments
13
+ from .formatter import Formatter
14
+ from .logger import Logger, Log
15
+ from .pipe import Pipe
16
+ from .settings import Settings
17
+
18
+
19
+ class Downloader:
20
+
21
+ def __init__(self):
22
+ self.helix_api = Helix(client_id=Settings().config['client_id'], bearer_token=Arguments().oauth_token, use_cache=True)
23
+
24
+ self.formats: List[str] = []
25
+ self.whitelist: List[str] = []
26
+ self.blacklist: List[str] = []
27
+
28
+ # Populate format list according to whitelist and blacklist
29
+ if 'all' in Arguments().formats and 'all' in Settings().config['formats']:
30
+ self.blacklist = Settings().config['formats']['all']['whitelist'] or []
31
+ self.whitelist = Settings().config['formats']['all']['blacklist'] or []
32
+
33
+ # Append formats to list if they can be used
34
+ self.formats = [format_name for format_name in Settings().config['formats'].keys() if
35
+ self._can_use_format(format_name)]
36
+
37
+ else:
38
+ self.formats = [format_name for format_name in Arguments().formats if self._can_use_format(format_name)]
39
+
40
+ def _can_use_format(self, format_name: str) -> bool:
41
+ """
42
+ Check if format name should be used based on whitelist and blacklist
43
+ :param format_name: Name of format
44
+ :return: If format should be used
45
+ """
46
+
47
+ # Lowercase format name
48
+ format_name = format_name.lower()
49
+
50
+ # Reserved format names
51
+ if format_name in ['all']:
52
+ return False
53
+
54
+ # Format does not exist
55
+ if format_name not in Settings().config['formats'].keys():
56
+ return False
57
+
58
+ # Whitelisted formats
59
+ if self.whitelist and format_name not in self.whitelist:
60
+ return False
61
+
62
+ # Blacklisted formats
63
+ if self.blacklist and format_name in self.blacklist:
64
+ return False
65
+
66
+ return True
67
+
68
+ def video(self, video: Video) -> None:
69
+ """
70
+ Download chat from video
71
+ :param video: Video object
72
+ :return: None
73
+ """
74
+
75
+ # Parse video duration
76
+ regex = re.compile(r'((?P<hours>\d+?)h)?((?P<minutes>\d+?)m)?((?P<seconds>\d+?)s)?')
77
+ parts = regex.match(video.duration).groupdict()
78
+
79
+ time_params = {}
80
+ for name, param in parts.items():
81
+ if param:
82
+ time_params[name] = int(param)
83
+
84
+ video_duration = datetime.timedelta(**time_params)
85
+
86
+ formatter = Formatter(video)
87
+
88
+ # Special case for JSON
89
+ # Build JSON object before writing it
90
+ if 'json' in self.formats:
91
+ Logger().log('Downloading JSON data', Log.VERBOSE)
92
+ output: str = Pipe(Settings().config['formats']['json']['output']).output(video.data)
93
+ os.makedirs(os.path.dirname(output), exist_ok=True)
94
+
95
+ data: dict = {
96
+ 'video': video.data,
97
+ 'comments': []
98
+ }
99
+
100
+ for comment in video.comments:
101
+
102
+ # Skip unspecified users if a list is provided.
103
+ if Arguments().users and comment.commenter.name.lower() not in Arguments().users:
104
+ continue
105
+
106
+ # If specified, only include messages that include a specified string
107
+ if Arguments().includes and Arguments().includes not in comment.message.body.lower():
108
+ continue
109
+
110
+ # Add comment to dictionary
111
+ data['comments'].append(comment.data)
112
+
113
+ # Ignore comments that were posted after the VOD finished
114
+ if Settings().config['formats']['json'].get('comments', {}).get('ignore_new_comments', False):
115
+ comment_date = dateutil.parser.parse(comment.created_at)
116
+ vod_finish_date = dateutil.parser.parse(video.created_at) + video_duration
117
+
118
+ if comment_date > vod_finish_date:
119
+ continue
120
+
121
+ if Logger().should_print_type(Log.PROGRESS):
122
+ self.draw_progress(current=comment.content_offset_seconds,
123
+ end=video_duration.seconds,
124
+ description='json')
125
+
126
+ with open(output, 'w', encoding='utf-8') as file:
127
+ json.dump(data, file, indent=4, sort_keys=True)
128
+
129
+ Logger().log(f'[json] {output}')
130
+
131
+ # For each format (ignore json this time)
132
+ for format_name in [x for x in self.formats if x not in ['json']]:
133
+ Logger().log(f'Formatting chat using: {format_name}', Log.VERBOSE)
134
+
135
+ # Get (formatted_comment, comment), output
136
+ comment_tuple, output = formatter.use(format_name)
137
+
138
+ # Create output directory and write to file
139
+ os.makedirs(os.path.dirname(output), exist_ok=True)
140
+ with open(output, '+w', encoding='utf-8') as file:
141
+ # Write Twitch URL as header at the beginning of the file
142
+ twitch_url = f"https://www.twitch.tv/videos/{video.id}?filter=archives&sort=time"
143
+ file.write(f"{twitch_url}\n\n")
144
+
145
+ # For every comment in video
146
+ for formatted_comment, comment in comment_tuple:
147
+
148
+ # Skip unspecified users if a list is provided.
149
+ if Arguments().users and comment.commenter.name.lower() not in Arguments().users:
150
+ continue
151
+
152
+ # If specified, only include messages that include a specified string
153
+ if Arguments().includes and Arguments().includes.lower() not in comment.message.body.lower():
154
+ continue
155
+
156
+ # Ignore comments that were posted after the VOD finished
157
+ if Settings().config['formats'][format_name].get('comments', {}).get('ignore_new_comments', False):
158
+ comment_date = dateutil.parser.parse(comment.created_at)
159
+ vod_finish_date = dateutil.parser.parse(video.created_at) + video_duration
160
+
161
+ if comment_date > vod_finish_date:
162
+ continue
163
+
164
+ # Draw progress
165
+ if comment and Logger().should_print_type(Log.PROGRESS):
166
+ self.draw_progress(current=comment.content_offset_seconds,
167
+ end=video_duration.seconds,
168
+ description=format_name)
169
+
170
+ # Display preview
171
+ Logger().log(formatted_comment, Log.PREVIEW)
172
+
173
+ # Write comment to file
174
+ file.write('{}\n'.format(formatted_comment))
175
+
176
+ Logger().log('[{}] {}'.format(format_name, output))
177
+
178
+ def videos(self, video_ids: List[int]) -> None:
179
+ """
180
+ Download multiple video ids
181
+ :param video_ids: List of video ids
182
+ :return: None
183
+ """
184
+ for video in self.helix_api.videos(video_ids):
185
+ Logger().log(format('\n{}'.format(video.title)), Log.REGULAR)
186
+ self.video(video)
187
+
188
+ def channels(self, channels: List[str]) -> None:
189
+ """
190
+ Download videos from multiple channels
191
+ :param channels: List of channel names
192
+ :return: None
193
+ """
194
+ Logger().log(f'Starting download with first={Arguments().first}', Log.DEBUG)
195
+ for channel, videos in self.helix_api.users(channels).videos(first=Arguments().first):
196
+ Logger().log(format('\n{}'.format(channel.display_name)), Log.REGULAR)
197
+ Logger().log(f'Videos object type: {type(videos)}', Log.DEBUG)
198
+ video_count = 0
199
+ try:
200
+ for video in videos:
201
+ video_count += 1
202
+ Logger().log(f'Processing video #{video_count}: {video.id}', Log.DEBUG)
203
+ Logger().log(format('\n{}'.format(video.title)), Log.REGULAR)
204
+ try:
205
+ self.video(video)
206
+ Logger().log(f'Successfully processed video {video.id}', Log.DEBUG)
207
+ except Exception as e:
208
+ Logger().log(f'Error processing video {video.id}: {e}', Log.ERROR)
209
+ import traceback
210
+ traceback.print_exc()
211
+ # Continue to next video instead of stopping
212
+ continue
213
+ except Exception as e:
214
+ Logger().log(f'Error iterating videos: {e}', Log.ERROR)
215
+ import traceback
216
+ traceback.print_exc()
217
+ Logger().log(f'Processed {video_count} videos for channel {channel.display_name}', Log.VERBOSE)
218
+
219
+ @staticmethod
220
+ def draw_progress(current: float, end: float, description: str = 'Downloading') -> None:
221
+ """
222
+ Draw download progress
223
+ :param current: Current chat position (seconds)
224
+ :param end: End position (seconds)
225
+ :param description: Progress description
226
+ :return:
227
+ """
228
+ sys.stdout.write('[{}] {}%\r'.format(description, '%.2f' % min(current * 10 / end * 10, 100.00)))
229
+ sys.stdout.flush()
File without changes
@@ -0,0 +1,30 @@
1
+ from typing import Generator, Tuple
2
+
3
+ from twitch.helix import Video
4
+ from twitch.v5 import Comments, Comment
5
+
6
+ from tcd.formats.format import Format
7
+ from tcd.pipe import Pipe
8
+
9
+
10
+ class Custom(Format):
11
+
12
+ def __init__(self, video: Video, format_name: str):
13
+ super().__init__(video, format_name)
14
+
15
+ def use(self) -> Tuple[Generator[Tuple[str, Comment], None, None], str]:
16
+ """
17
+ Use this format
18
+ :return: tuple(formatted comment, comment), output format
19
+ """
20
+ # Format comments
21
+ comments = self.comment_generator(self.video.comments)
22
+
23
+ # Format output
24
+ output: str = Pipe(self.format_dictionary['output']).output(self.video.data)
25
+
26
+ return comments, output
27
+
28
+ def comment_generator(self, comments: Comments) -> Generator[Tuple[str, Comment], None, None]:
29
+ for comment in comments:
30
+ yield Pipe(self.format_dictionary['comments']).comment(comment.data), comment
@@ -0,0 +1,11 @@
1
+ from twitch.helix import Video
2
+
3
+ from tcd.settings import Settings
4
+
5
+
6
+ class Format:
7
+
8
+ def __init__(self, video: Video, format_name: str):
9
+ self.video: Video = video
10
+ self.format_name: str = format_name
11
+ self.format_dictionary: dict = Settings().config['formats'][format_name]