tcd2 3.2.2.post1__py3-none-any.whl → 3.2.2.post2__py3-none-any.whl
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.
- tcd/__init__.py +1 -1
- tcd/comments.py +13 -0
- tcd/downloader.py +7 -6
- tcd/formats/custom.py +17 -17
- tcd/formats/format.py +20 -10
- tcd/formats/srt.py +21 -21
- tcd/formats/ssa.py +8 -8
- tcd/graphql_comments.py +211 -0
- {tcd2-3.2.2.post1.dist-info → tcd2-3.2.2.post2.dist-info}/METADATA +1 -1
- tcd2-3.2.2.post2.dist-info/RECORD +24 -0
- tcd2-3.2.2.post1.dist-info/RECORD +0 -22
- {tcd2-3.2.2.post1.dist-info → tcd2-3.2.2.post2.dist-info}/WHEEL +0 -0
- {tcd2-3.2.2.post1.dist-info → tcd2-3.2.2.post2.dist-info}/entry_points.txt +0 -0
- {tcd2-3.2.2.post1.dist-info → tcd2-3.2.2.post2.dist-info}/licenses/LICENSE +0 -0
- {tcd2-3.2.2.post1.dist-info → tcd2-3.2.2.post2.dist-info}/top_level.txt +0 -0
tcd/__init__.py
CHANGED
tcd/comments.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from typing import Iterable
|
|
2
|
+
|
|
3
|
+
from twitch.helix import Video
|
|
4
|
+
from twitch.v5 import Comment
|
|
5
|
+
|
|
6
|
+
from .graphql_comments import GraphQLComments
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_comments(video: Video) -> Iterable[Comment]:
|
|
10
|
+
"""
|
|
11
|
+
Return an iterator of comments for a video using Twitch GraphQL.
|
|
12
|
+
"""
|
|
13
|
+
return GraphQLComments(video.id)
|
tcd/downloader.py
CHANGED
|
@@ -9,11 +9,12 @@ import dateutil
|
|
|
9
9
|
from twitch import Helix
|
|
10
10
|
from twitch.helix import Video
|
|
11
11
|
|
|
12
|
-
from .arguments import Arguments
|
|
13
|
-
from .
|
|
14
|
-
from .
|
|
15
|
-
from .
|
|
16
|
-
from .
|
|
12
|
+
from .arguments import Arguments
|
|
13
|
+
from .comments import get_comments
|
|
14
|
+
from .formatter import Formatter
|
|
15
|
+
from .logger import Logger, Log
|
|
16
|
+
from .pipe import Pipe
|
|
17
|
+
from .settings import Settings
|
|
17
18
|
|
|
18
19
|
|
|
19
20
|
class Downloader:
|
|
@@ -97,7 +98,7 @@ class Downloader:
|
|
|
97
98
|
'comments': []
|
|
98
99
|
}
|
|
99
100
|
|
|
100
|
-
for comment in video
|
|
101
|
+
for comment in get_comments(video):
|
|
101
102
|
|
|
102
103
|
# Skip unspecified users if a list is provided.
|
|
103
104
|
if Arguments().users and comment.commenter.name.lower() not in Arguments().users:
|
tcd/formats/custom.py
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
from typing import Generator, Tuple
|
|
2
|
-
|
|
3
|
-
from twitch.helix import Video
|
|
4
|
-
from twitch.v5 import
|
|
5
|
-
|
|
6
|
-
from tcd.formats.format import Format
|
|
7
|
-
from tcd.pipe import Pipe
|
|
1
|
+
from typing import Generator, Tuple, Iterable
|
|
2
|
+
|
|
3
|
+
from twitch.helix import Video
|
|
4
|
+
from twitch.v5 import Comment
|
|
5
|
+
|
|
6
|
+
from tcd.formats.format import Format
|
|
7
|
+
from tcd.pipe import Pipe
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
class Custom(Format):
|
|
@@ -12,19 +12,19 @@ class Custom(Format):
|
|
|
12
12
|
def __init__(self, video: Video, format_name: str):
|
|
13
13
|
super().__init__(video, format_name)
|
|
14
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.
|
|
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.comments())
|
|
22
22
|
|
|
23
23
|
# Format output
|
|
24
24
|
output: str = Pipe(self.format_dictionary['output']).output(self.video.data)
|
|
25
25
|
|
|
26
26
|
return comments, output
|
|
27
27
|
|
|
28
|
-
def comment_generator(self, comments:
|
|
29
|
-
for comment in comments:
|
|
30
|
-
yield Pipe(self.format_dictionary['comments']).comment(comment.data), comment
|
|
28
|
+
def comment_generator(self, comments: Iterable[Comment]) -> Generator[Tuple[str, Comment], None, None]:
|
|
29
|
+
for comment in comments:
|
|
30
|
+
yield Pipe(self.format_dictionary['comments']).comment(comment.data), comment
|
tcd/formats/format.py
CHANGED
|
@@ -1,11 +1,21 @@
|
|
|
1
|
-
from
|
|
1
|
+
from typing import Iterable
|
|
2
|
+
|
|
3
|
+
from twitch.helix import Video
|
|
4
|
+
from twitch.v5 import Comment
|
|
5
|
+
|
|
6
|
+
from tcd.comments import get_comments
|
|
7
|
+
from tcd.settings import Settings
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Format:
|
|
2
11
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
def
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
+
def __init__(self, video: Video, format_name: str):
|
|
13
|
+
self.video: Video = video
|
|
14
|
+
self.format_name: str = format_name
|
|
15
|
+
self.format_dictionary: dict = Settings().config['formats'][format_name]
|
|
16
|
+
|
|
17
|
+
def comments(self) -> Iterable[Comment]:
|
|
18
|
+
"""
|
|
19
|
+
Return an iterator for video comments using GraphQL.
|
|
20
|
+
"""
|
|
21
|
+
return get_comments(self.video)
|
tcd/formats/srt.py
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import datetime
|
|
2
|
-
from typing import Tuple, Generator
|
|
3
|
-
|
|
4
|
-
from twitch.helix import Video
|
|
5
|
-
from twitch.v5 import Comment
|
|
6
|
-
|
|
7
|
-
from tcd.formats.format import Format
|
|
8
|
-
from tcd.pipe import Pipe
|
|
9
|
-
from tcd.safedict import SafeDict
|
|
1
|
+
import datetime
|
|
2
|
+
from typing import Tuple, Generator, Iterable
|
|
3
|
+
|
|
4
|
+
from twitch.helix import Video
|
|
5
|
+
from twitch.v5 import Comment
|
|
6
|
+
|
|
7
|
+
from tcd.formats.format import Format
|
|
8
|
+
from tcd.pipe import Pipe
|
|
9
|
+
from tcd.safedict import SafeDict
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
class SRT(Format):
|
|
@@ -18,12 +18,12 @@ class SRT(Format):
|
|
|
18
18
|
"""
|
|
19
19
|
super().__init__(video, format_name='srt')
|
|
20
20
|
|
|
21
|
-
def use(self) -> Tuple[Generator[Tuple[str, Comment], None, None], str]:
|
|
22
|
-
"""
|
|
23
|
-
Use SRT format
|
|
24
|
-
:return: Comment generator and output string
|
|
25
|
-
"""
|
|
26
|
-
return self.subtitles(self.
|
|
21
|
+
def use(self) -> Tuple[Generator[Tuple[str, Comment], None, None], str]:
|
|
22
|
+
"""
|
|
23
|
+
Use SRT format
|
|
24
|
+
:return: Comment generator and output string
|
|
25
|
+
"""
|
|
26
|
+
return self.subtitles(self.comments()), Pipe(self.format_dictionary['output']).output(self.video.data)
|
|
27
27
|
|
|
28
28
|
@staticmethod
|
|
29
29
|
def format_timestamp(time: datetime.timedelta) -> str:
|
|
@@ -45,12 +45,12 @@ class SRT(Format):
|
|
|
45
45
|
|
|
46
46
|
return f'{int(hours):02d}:{int(minutes):02d}:{int(seconds):02d},{milliseconds:03d}'
|
|
47
47
|
|
|
48
|
-
def subtitles(self, comments:
|
|
49
|
-
"""
|
|
50
|
-
Subtitle generator
|
|
51
|
-
:param comments: Comments to turn into subtitles
|
|
52
|
-
:return: Generator with subtitles and subtitle data
|
|
53
|
-
"""
|
|
48
|
+
def subtitles(self, comments: Iterable[Comment]) -> Generator[Tuple[str, Comment], None, None]:
|
|
49
|
+
"""
|
|
50
|
+
Subtitle generator
|
|
51
|
+
:param comments: Comments to turn into subtitles
|
|
52
|
+
:return: Generator with subtitles and subtitle data
|
|
53
|
+
"""
|
|
54
54
|
for index, comment in enumerate(comments):
|
|
55
55
|
# Stat and stop timestamps. Add a millisecond for timedelta to include millisecond digits
|
|
56
56
|
start = datetime.timedelta(seconds=comment.content_offset_seconds)
|
tcd/formats/ssa.py
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import datetime
|
|
2
|
-
from itertools import chain
|
|
3
|
-
from typing import Tuple, Generator, List, Optional
|
|
1
|
+
import datetime
|
|
2
|
+
from itertools import chain
|
|
3
|
+
from typing import Tuple, Generator, List, Optional, Iterable
|
|
4
4
|
|
|
5
|
-
from twitch.helix import Video
|
|
6
|
-
from twitch.v5 import Comment
|
|
5
|
+
from twitch.helix import Video
|
|
6
|
+
from twitch.v5 import Comment
|
|
7
7
|
|
|
8
8
|
from tcd.formats.format import Format
|
|
9
9
|
from tcd.pipe import Pipe
|
|
@@ -32,8 +32,8 @@ class SSA(Format):
|
|
|
32
32
|
Line generator
|
|
33
33
|
:return:
|
|
34
34
|
"""
|
|
35
|
-
for line in chain(self.prefix(), self.dialogues(self.
|
|
36
|
-
yield line
|
|
35
|
+
for line in chain(self.prefix(), self.dialogues(self.comments())):
|
|
36
|
+
yield line
|
|
37
37
|
|
|
38
38
|
@staticmethod
|
|
39
39
|
def format_timestamp(time: datetime.timedelta) -> str:
|
|
@@ -55,7 +55,7 @@ class SSA(Format):
|
|
|
55
55
|
|
|
56
56
|
return f'{int(hours):01d}:{int(minutes):02d}:{int(seconds):02d}.{centiseconds:02d}'
|
|
57
57
|
|
|
58
|
-
def dialogues(self, comments:
|
|
58
|
+
def dialogues(self, comments: Iterable[Comment]) -> Generator[Tuple[str, Comment], None, None]:
|
|
59
59
|
"""
|
|
60
60
|
Format comments as SSA dialogues
|
|
61
61
|
:param comments: Comment to format
|
tcd/graphql_comments.py
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
"""
|
|
2
|
+
GraphQL-based comments module for Twitch VODs.
|
|
3
|
+
This replaces the deprecated V5 API with Twitch's GraphQL API.
|
|
4
|
+
"""
|
|
5
|
+
from typing import Union, Generator, Dict, Any, List
|
|
6
|
+
import threading
|
|
7
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
8
|
+
|
|
9
|
+
import requests
|
|
10
|
+
import twitch.v5 as v5
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class GraphQLComments:
|
|
14
|
+
"""
|
|
15
|
+
Fetches VOD comments using Twitch's GraphQL API.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
GRAPHQL_ENDPOINT = 'https://gql.twitch.tv/gql'
|
|
19
|
+
# Public client ID used by Twitch web
|
|
20
|
+
CLIENT_ID = 'kimne78kx3ncx6brgo4mv6wki5h1ko'
|
|
21
|
+
# SHA256 hash for the VideoCommentsByOffsetOrCursor persisted query
|
|
22
|
+
QUERY_HASH = 'b70a3591ff0f4e0313d126c6a1502d79a1c02baebb288227c582044aa76adf6a'
|
|
23
|
+
|
|
24
|
+
def __init__(self, video_id: Union[str, int], num_threads: int = 4):
|
|
25
|
+
self._video_id: str = str(video_id)
|
|
26
|
+
self._num_threads: int = num_threads
|
|
27
|
+
self._session = requests.Session()
|
|
28
|
+
self._session_lock = threading.Lock()
|
|
29
|
+
|
|
30
|
+
def _graphql_request(self, variables: Dict[str, Any]) -> Dict[str, Any]:
|
|
31
|
+
query = [{
|
|
32
|
+
"operationName": "VideoCommentsByOffsetOrCursor",
|
|
33
|
+
"variables": variables,
|
|
34
|
+
"extensions": {
|
|
35
|
+
"persistedQuery": {
|
|
36
|
+
"version": 1,
|
|
37
|
+
"sha256Hash": self.QUERY_HASH
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}]
|
|
41
|
+
|
|
42
|
+
headers = {
|
|
43
|
+
'Client-Id': self.CLIENT_ID,
|
|
44
|
+
'Content-Type': 'application/json'
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
with self._session_lock:
|
|
48
|
+
response = self._session.post(self.GRAPHQL_ENDPOINT, json=query, headers=headers)
|
|
49
|
+
response.raise_for_status()
|
|
50
|
+
return response.json()
|
|
51
|
+
|
|
52
|
+
def _fetch_chunk(self, start_offset: float) -> List[Dict[str, Any]]:
|
|
53
|
+
comments: List[Dict[str, Any]] = []
|
|
54
|
+
variables = {
|
|
55
|
+
"videoID": self._video_id,
|
|
56
|
+
"contentOffsetSeconds": start_offset
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
try:
|
|
60
|
+
response_data = self._graphql_request(variables)
|
|
61
|
+
if not response_data:
|
|
62
|
+
return comments
|
|
63
|
+
|
|
64
|
+
data = response_data[0].get('data', {})
|
|
65
|
+
video_data = data.get('video')
|
|
66
|
+
if not video_data:
|
|
67
|
+
return comments
|
|
68
|
+
|
|
69
|
+
comments_data = video_data.get('comments')
|
|
70
|
+
if not comments_data:
|
|
71
|
+
return comments
|
|
72
|
+
|
|
73
|
+
edges = comments_data.get('edges', [])
|
|
74
|
+
for edge in edges:
|
|
75
|
+
comments.append(self._convert_graphql_comment_to_v5_format(edge))
|
|
76
|
+
except Exception as exc:
|
|
77
|
+
import sys
|
|
78
|
+
print(f"Error fetching chunk at offset {start_offset}: {exc}", file=sys.stderr)
|
|
79
|
+
|
|
80
|
+
return comments
|
|
81
|
+
|
|
82
|
+
def _convert_graphql_comment_to_v5_format(self, comment_node: Dict[str, Any]) -> Dict[str, Any]:
|
|
83
|
+
comment = comment_node.get('node', {})
|
|
84
|
+
commenter = comment.get('commenter', {})
|
|
85
|
+
message = comment.get('message', {})
|
|
86
|
+
|
|
87
|
+
fragments = []
|
|
88
|
+
emoticons = []
|
|
89
|
+
full_message_text = []
|
|
90
|
+
|
|
91
|
+
for fragment in message.get('fragments', []):
|
|
92
|
+
fragment_text = fragment.get('text', '')
|
|
93
|
+
full_message_text.append(fragment_text)
|
|
94
|
+
fragment_data = {'text': fragment_text}
|
|
95
|
+
|
|
96
|
+
emote = fragment.get('emote')
|
|
97
|
+
if emote:
|
|
98
|
+
emoticon_data = {
|
|
99
|
+
'_id': emote.get('emoteID'),
|
|
100
|
+
'begin': len(''.join([f['text'] for f in fragments])),
|
|
101
|
+
'end': None,
|
|
102
|
+
'emoticon_id': emote.get('emoteID'),
|
|
103
|
+
'emoticon_set_id': None
|
|
104
|
+
}
|
|
105
|
+
emoticon_data['end'] = emoticon_data['begin'] + len(fragment_data['text'])
|
|
106
|
+
emoticons.append(emoticon_data)
|
|
107
|
+
fragment_data['emoticon'] = emoticon_data
|
|
108
|
+
|
|
109
|
+
fragments.append(fragment_data)
|
|
110
|
+
|
|
111
|
+
message_body = ''.join(full_message_text)
|
|
112
|
+
|
|
113
|
+
user_badges = []
|
|
114
|
+
for badge in message.get('userBadges', []):
|
|
115
|
+
user_badges.append({
|
|
116
|
+
'_id': badge.get('setID'),
|
|
117
|
+
'version': badge.get('version')
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
v5_comment = {
|
|
121
|
+
'_id': comment.get('id'),
|
|
122
|
+
'created_at': comment.get('createdAt'),
|
|
123
|
+
'updated_at': comment.get('updatedAt', comment.get('createdAt')),
|
|
124
|
+
'channel_id': None,
|
|
125
|
+
'content_type': 'video',
|
|
126
|
+
'content_id': self._video_id,
|
|
127
|
+
'content_offset_seconds': float(comment.get('contentOffsetSeconds', 0)),
|
|
128
|
+
'commenter': {
|
|
129
|
+
'display_name': commenter.get('displayName'),
|
|
130
|
+
'_id': commenter.get('id'),
|
|
131
|
+
'name': commenter.get('login'),
|
|
132
|
+
'type': None,
|
|
133
|
+
'bio': None,
|
|
134
|
+
'created_at': None,
|
|
135
|
+
'updated_at': None,
|
|
136
|
+
'logo': None
|
|
137
|
+
} if commenter else None,
|
|
138
|
+
'source': 'chat',
|
|
139
|
+
'state': 'published',
|
|
140
|
+
'message': {
|
|
141
|
+
'body': message_body,
|
|
142
|
+
'emoticons': emoticons,
|
|
143
|
+
'fragments': fragments,
|
|
144
|
+
'is_action': False,
|
|
145
|
+
'user_badges': user_badges,
|
|
146
|
+
'user_color': message.get('userColor')
|
|
147
|
+
},
|
|
148
|
+
'more_replies': False
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return v5_comment
|
|
152
|
+
|
|
153
|
+
def __iter__(self) -> Generator['v5.Comment', None, None]:
|
|
154
|
+
initial_chunk = self._fetch_chunk(0.0)
|
|
155
|
+
if not initial_chunk:
|
|
156
|
+
return
|
|
157
|
+
|
|
158
|
+
chunk_interval = 3600
|
|
159
|
+
all_comments: List[Dict[str, Any]] = []
|
|
160
|
+
current_offset = 0.0
|
|
161
|
+
max_offset = 0.0
|
|
162
|
+
|
|
163
|
+
while True:
|
|
164
|
+
chunk_offsets = []
|
|
165
|
+
for i in range(self._num_threads):
|
|
166
|
+
offset = current_offset + (i * chunk_interval)
|
|
167
|
+
chunk_offsets.append(offset)
|
|
168
|
+
|
|
169
|
+
chunks_data = []
|
|
170
|
+
with ThreadPoolExecutor(max_workers=self._num_threads) as executor:
|
|
171
|
+
future_to_offset = {
|
|
172
|
+
executor.submit(self._fetch_chunk, offset): offset
|
|
173
|
+
for offset in chunk_offsets
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
for future in as_completed(future_to_offset):
|
|
177
|
+
offset = future_to_offset[future]
|
|
178
|
+
try:
|
|
179
|
+
chunk_comments = future.result()
|
|
180
|
+
if chunk_comments:
|
|
181
|
+
chunks_data.append((offset, chunk_comments))
|
|
182
|
+
except Exception as exc:
|
|
183
|
+
import sys
|
|
184
|
+
print(f"Error downloading chunk at {offset}: {exc}", file=sys.stderr)
|
|
185
|
+
|
|
186
|
+
chunks_data.sort(key=lambda x: x[0])
|
|
187
|
+
|
|
188
|
+
got_new_data = False
|
|
189
|
+
for _, chunk_comments in chunks_data:
|
|
190
|
+
for comment_data in chunk_comments:
|
|
191
|
+
comment_offset = float(comment_data.get('content_offset_seconds', 0))
|
|
192
|
+
if comment_offset <= max_offset:
|
|
193
|
+
continue
|
|
194
|
+
|
|
195
|
+
all_comments.append(comment_data)
|
|
196
|
+
max_offset = max(max_offset, comment_offset)
|
|
197
|
+
got_new_data = True
|
|
198
|
+
|
|
199
|
+
if not got_new_data:
|
|
200
|
+
break
|
|
201
|
+
|
|
202
|
+
current_offset = max_offset
|
|
203
|
+
|
|
204
|
+
all_comments.sort(key=lambda x: x.get('content_offset_seconds', 0))
|
|
205
|
+
|
|
206
|
+
from twitch.api import API
|
|
207
|
+
minimal_api = API(base_url='https://api.twitch.tv/v5/',
|
|
208
|
+
client_id=self.CLIENT_ID)
|
|
209
|
+
|
|
210
|
+
for comment_data in all_comments:
|
|
211
|
+
yield v5.Comment(api=minimal_api, data=comment_data)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
tcd/__init__.py,sha256=UR76UQ1ofdIZtdYsWegE5FcU9e2NYeTd9HvUHEBsXR4,4944
|
|
2
|
+
tcd/__main__.py,sha256=r-uXw3zgKZ6lSdY61Q36HFSMTjzc6shN_W6F2x7gkDc,88
|
|
3
|
+
tcd/arguments.py,sha256=Vl9PnEPelrksAV8BNpYlHPqm0JE51qgR7_9yEZpaLmU,3283
|
|
4
|
+
tcd/comments.py,sha256=UCBJMs1nfedH3hCAn24r9L0Zi35VEMDuFG9tvOtIXA0,314
|
|
5
|
+
tcd/downloader.py,sha256=_RHe0EZy2USklXFj-oIbIpEDmBR6SuIdVFThAQlfv2U,9558
|
|
6
|
+
tcd/formatter.py,sha256=FilDWcys7nD-5b96eTYxMBA3TeHujOrnv8Is-nYn8GY,903
|
|
7
|
+
tcd/graphql_comments.py,sha256=vlMDyrzGH5tr2Xp5x95dq9oVWda1XgY88TfyZTK_JAQ,7476
|
|
8
|
+
tcd/logger.py,sha256=nnIrv0_-1ACy-32oel0PsfhMxc0s9H7Qu80SeyAN8rU,3362
|
|
9
|
+
tcd/pipe.py,sha256=U3VuSES3r_wG0zsq0eY_S2QJ4RoumKrYqpv8mI4_FLA,10567
|
|
10
|
+
tcd/safedict.py,sha256=_2EM-9LGA3JrSvQb7Z75aNeBDFQBYbsBa3TREaSNvOU,263
|
|
11
|
+
tcd/settings.py,sha256=_yxe1eC7jltc5dRRCZkx7ImyG5D_tWnnJnRCC58zsBY,4091
|
|
12
|
+
tcd/settings.reference.json,sha256=zQS5Bb7ZlaZSOMbLBG00IZvKNRXkQM6rQ_4vLUlnmGY,3230
|
|
13
|
+
tcd/singleton.py,sha256=PGSVYql6sCbrTsZVCZOTUufndCBfqRy0-6OQCaXIbII,371
|
|
14
|
+
tcd/formats/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
15
|
+
tcd/formats/custom.py,sha256=Jr2tVZgBJo_nPxY7-PhJLIDJeuZ0H_zMCY5phMqtgmY,964
|
|
16
|
+
tcd/formats/format.py,sha256=LBgBKwQdcozvsgwfxFRoACVsZDvyi4C2IJK-cIkurEM,567
|
|
17
|
+
tcd/formats/srt.py,sha256=RQL17u0nvV9pIMzbS2miFDvVDLfSCSjrpYLJUiSbXRs,2643
|
|
18
|
+
tcd/formats/ssa.py,sha256=uiHfJ50vJsewJYYztsdJIvLimoCLUUrVNYnMuwtrXNg,7064
|
|
19
|
+
tcd2-3.2.2.post2.dist-info/licenses/LICENSE,sha256=Ch-ZtxzUuhHcxKI4Yq4L_en60uZKBBpMamPeImjzpQM,1093
|
|
20
|
+
tcd2-3.2.2.post2.dist-info/METADATA,sha256=AxKDvI-nuUriIj7mOPl6BdF2kcv0jG9MtGmmMiH-704,1622
|
|
21
|
+
tcd2-3.2.2.post2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
22
|
+
tcd2-3.2.2.post2.dist-info/entry_points.txt,sha256=47ljm618xZuZ07uL5o9HYnJ8ZN8m0kFptoMIjzYy8cI,33
|
|
23
|
+
tcd2-3.2.2.post2.dist-info/top_level.txt,sha256=HF0kJO8nLMAOZIROY5suwglVvTWTXhkycR2TjicOJSA,4
|
|
24
|
+
tcd2-3.2.2.post2.dist-info/RECORD,,
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
tcd/__init__.py,sha256=56y8gP6QGKvOpl2Tl6UB_OW8ewlq4y9PEtwoiNmzvfI,4944
|
|
2
|
-
tcd/__main__.py,sha256=r-uXw3zgKZ6lSdY61Q36HFSMTjzc6shN_W6F2x7gkDc,88
|
|
3
|
-
tcd/arguments.py,sha256=Vl9PnEPelrksAV8BNpYlHPqm0JE51qgR7_9yEZpaLmU,3283
|
|
4
|
-
tcd/downloader.py,sha256=-EU4Nh_nvDARXG3Lc5pbDI1Dm4RHUS4XXtIStizzf8k,9524
|
|
5
|
-
tcd/formatter.py,sha256=FilDWcys7nD-5b96eTYxMBA3TeHujOrnv8Is-nYn8GY,903
|
|
6
|
-
tcd/logger.py,sha256=nnIrv0_-1ACy-32oel0PsfhMxc0s9H7Qu80SeyAN8rU,3362
|
|
7
|
-
tcd/pipe.py,sha256=U3VuSES3r_wG0zsq0eY_S2QJ4RoumKrYqpv8mI4_FLA,10567
|
|
8
|
-
tcd/safedict.py,sha256=_2EM-9LGA3JrSvQb7Z75aNeBDFQBYbsBa3TREaSNvOU,263
|
|
9
|
-
tcd/settings.py,sha256=_yxe1eC7jltc5dRRCZkx7ImyG5D_tWnnJnRCC58zsBY,4091
|
|
10
|
-
tcd/settings.reference.json,sha256=zQS5Bb7ZlaZSOMbLBG00IZvKNRXkQM6rQ_4vLUlnmGY,3230
|
|
11
|
-
tcd/singleton.py,sha256=PGSVYql6sCbrTsZVCZOTUufndCBfqRy0-6OQCaXIbII,371
|
|
12
|
-
tcd/formats/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
|
-
tcd/formats/custom.py,sha256=_s8vY1CuYPLSwbV4KMRph0QHTekNNmERPh5DgyE9MaI,976
|
|
14
|
-
tcd/formats/format.py,sha256=BzUJluE-SUVQ5XGayVEsnOD14TKirb5YGo9nvS-md04,309
|
|
15
|
-
tcd/formats/srt.py,sha256=W7sRVq4opVNhFvPy94w1lBK_QSJ1qFhreh-0devRPOE,2659
|
|
16
|
-
tcd/formats/ssa.py,sha256=9uB_5IcUxjECs55iuTfKgGAzGDHMASIzl5HkRSeG-e8,7068
|
|
17
|
-
tcd2-3.2.2.post1.dist-info/licenses/LICENSE,sha256=Ch-ZtxzUuhHcxKI4Yq4L_en60uZKBBpMamPeImjzpQM,1093
|
|
18
|
-
tcd2-3.2.2.post1.dist-info/METADATA,sha256=R2KQ2VN8xOhxS-fQHaM9ZKCArKAspEh6L-6xF_Yb6fk,1622
|
|
19
|
-
tcd2-3.2.2.post1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
20
|
-
tcd2-3.2.2.post1.dist-info/entry_points.txt,sha256=47ljm618xZuZ07uL5o9HYnJ8ZN8m0kFptoMIjzYy8cI,33
|
|
21
|
-
tcd2-3.2.2.post1.dist-info/top_level.txt,sha256=HF0kJO8nLMAOZIROY5suwglVvTWTXhkycR2TjicOJSA,4
|
|
22
|
-
tcd2-3.2.2.post1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|