viihdexdash 0.2.0__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.
- viihdexdash/__init__.py +0 -0
- viihdexdash/__main__.py +492 -0
- viihdexdash-0.2.0.dist-info/METADATA +99 -0
- viihdexdash-0.2.0.dist-info/RECORD +8 -0
- viihdexdash-0.2.0.dist-info/WHEEL +5 -0
- viihdexdash-0.2.0.dist-info/entry_points.txt +2 -0
- viihdexdash-0.2.0.dist-info/licenses/LICENSE +21 -0
- viihdexdash-0.2.0.dist-info/top_level.txt +1 -0
viihdexdash/__init__.py
ADDED
|
File without changes
|
viihdexdash/__main__.py
ADDED
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
# Copyright (c) 2026 Qotscha
|
|
2
|
+
|
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
4
|
+
# of this software and associated documentation files (the "Software"), to deal
|
|
5
|
+
# in the Software without restriction, including without limitation the rights
|
|
6
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
7
|
+
# copies of the Software, and to permit persons to whom the Software is
|
|
8
|
+
# furnished to do so, subject to the following conditions:
|
|
9
|
+
|
|
10
|
+
# The above copyright notice and this permission notice shall be included in all
|
|
11
|
+
# copies or substantial portions of the Software.
|
|
12
|
+
|
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
14
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
15
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
16
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
17
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
18
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
19
|
+
# SOFTWARE.
|
|
20
|
+
|
|
21
|
+
import argparse
|
|
22
|
+
import configparser
|
|
23
|
+
import locale
|
|
24
|
+
import os
|
|
25
|
+
import platform
|
|
26
|
+
import re
|
|
27
|
+
import sys
|
|
28
|
+
import warnings
|
|
29
|
+
import xml.etree.ElementTree as ET
|
|
30
|
+
from subprocess import Popen
|
|
31
|
+
import requests
|
|
32
|
+
import langcodes
|
|
33
|
+
from pycaption import DFXPReader, SRTWriter
|
|
34
|
+
|
|
35
|
+
from bs4 import XMLParsedAsHTMLWarning
|
|
36
|
+
warnings.filterwarnings("ignore", category=XMLParsedAsHTMLWarning)
|
|
37
|
+
|
|
38
|
+
if os.name == 'nt':
|
|
39
|
+
os.system('color')
|
|
40
|
+
|
|
41
|
+
finnish = bool(locale.getlocale()[0].split('_')[0] == 'Finnish')
|
|
42
|
+
|
|
43
|
+
def is_best_variant(b, w, h, cb, mb, mw, mh):
|
|
44
|
+
if (mb and b > mb) or (mw and w > mw) or (mh and h > mh):
|
|
45
|
+
return False
|
|
46
|
+
elif b > cb:
|
|
47
|
+
return True
|
|
48
|
+
else:
|
|
49
|
+
return False
|
|
50
|
+
|
|
51
|
+
def get_language(lang_code):
|
|
52
|
+
lang = langcodes.Language.get(lang_code)
|
|
53
|
+
return lang
|
|
54
|
+
|
|
55
|
+
def get_iso(language, to_iso = True, iso = 'alpha_3b'):
|
|
56
|
+
if not to_iso and '-' in str(language):
|
|
57
|
+
lcode = str(language)
|
|
58
|
+
elif iso == 'alpha_2':
|
|
59
|
+
lcode = language.language
|
|
60
|
+
elif iso == 'alpha_3':
|
|
61
|
+
lcode = langcodes.Language.get(language).to_alpha3(variant='t')
|
|
62
|
+
elif iso == 'alpha_3b':
|
|
63
|
+
lcode = langcodes.Language.get(language).to_alpha3(variant='b')
|
|
64
|
+
return lcode
|
|
65
|
+
|
|
66
|
+
def lang_in_langs(lang, langs):
|
|
67
|
+
l = False
|
|
68
|
+
for x in langs:
|
|
69
|
+
if lang.language == x.language:
|
|
70
|
+
fields = [k for k, v in vars(x).items() if not k.startswith('_') and v is not None]
|
|
71
|
+
l = True
|
|
72
|
+
for f in fields:
|
|
73
|
+
if lang[f] != x[f]:
|
|
74
|
+
l = False
|
|
75
|
+
break
|
|
76
|
+
return l
|
|
77
|
+
|
|
78
|
+
def list_tracks(available, wanted, default_list, dl_always = False):
|
|
79
|
+
dl_list = []
|
|
80
|
+
defaults = []
|
|
81
|
+
if wanted:
|
|
82
|
+
for x in wanted:
|
|
83
|
+
for y in available:
|
|
84
|
+
if lang_in_langs(y, [x]):
|
|
85
|
+
dl_list.append(y)
|
|
86
|
+
else:
|
|
87
|
+
for x in default_list:
|
|
88
|
+
for y in available:
|
|
89
|
+
if lang_in_langs(y, [x]):
|
|
90
|
+
dl_list.append(y)
|
|
91
|
+
dl_list += [x for x in available if not lang_in_langs(x, default_list)]
|
|
92
|
+
|
|
93
|
+
for x in default_list:
|
|
94
|
+
default_found = False
|
|
95
|
+
if dl_always and not dl_list:
|
|
96
|
+
for y in available:
|
|
97
|
+
if lang_in_langs(y, [x]):
|
|
98
|
+
dl_list.append(y)
|
|
99
|
+
defaults.append(y)
|
|
100
|
+
default_found = True
|
|
101
|
+
if default_found:
|
|
102
|
+
break
|
|
103
|
+
else:
|
|
104
|
+
for y in dl_list:
|
|
105
|
+
if lang_in_langs(y, [x]):
|
|
106
|
+
defaults.append(y)
|
|
107
|
+
default_found = True
|
|
108
|
+
if default_found:
|
|
109
|
+
break
|
|
110
|
+
if dl_always and not dl_list:
|
|
111
|
+
dl_list = [available[0]]
|
|
112
|
+
return dl_list, defaults
|
|
113
|
+
|
|
114
|
+
def is_hi_sub(name):
|
|
115
|
+
sub_names = ['ohjelmatekstitys', 'hard of hearing']
|
|
116
|
+
for x in sub_names:
|
|
117
|
+
if x in name:
|
|
118
|
+
return True
|
|
119
|
+
else:
|
|
120
|
+
return False
|
|
121
|
+
|
|
122
|
+
def download_sub(name, sub_url, tl):
|
|
123
|
+
srt = ''
|
|
124
|
+
for i, t in enumerate(range (int(tl['t']), (int(tl['r']) + 1) * int(tl['d']), int(tl['d']))):
|
|
125
|
+
r = requests.get(sub_url.replace('$Time$', str(t)))
|
|
126
|
+
frag = r.content.decode(encoding = 'utf-8', errors = 'ignore')
|
|
127
|
+
d = frag.split('mdat', 1)[1]
|
|
128
|
+
# print(t)
|
|
129
|
+
# print(d)
|
|
130
|
+
try:
|
|
131
|
+
ttml = DFXPReader().read(d)
|
|
132
|
+
except:
|
|
133
|
+
continue
|
|
134
|
+
sr = SRTWriter().write(ttml)
|
|
135
|
+
srt += sr
|
|
136
|
+
srt += '\n'
|
|
137
|
+
|
|
138
|
+
j = 1
|
|
139
|
+
sl = srt.splitlines()
|
|
140
|
+
sl.pop()
|
|
141
|
+
pattern = r"^\d{2}:\d{2}:\d{2},\d{3} --> \d{2}:\d{2}:\d{2},\d{3}$"
|
|
142
|
+
for i, l in enumerate(sl):
|
|
143
|
+
if l.isdigit() and bool(re.fullmatch(pattern, sl[i+1].strip())):
|
|
144
|
+
sl[i] = l.replace(l, str(j))
|
|
145
|
+
j += 1
|
|
146
|
+
|
|
147
|
+
with open (name, 'w', encoding="utf-8") as f:
|
|
148
|
+
f.writelines(line + '\n' for line in sl)
|
|
149
|
+
if finnish:
|
|
150
|
+
print('Tekstitys ladattu.')
|
|
151
|
+
else:
|
|
152
|
+
print('Subtitle downloaded')
|
|
153
|
+
|
|
154
|
+
def create_config(config_path, config = None, write_config = True):
|
|
155
|
+
config_changed = False
|
|
156
|
+
default_config = configparser.ConfigParser()
|
|
157
|
+
default_config['Download settings'] = { 'audio languages': '',
|
|
158
|
+
'subtitle languages': '',
|
|
159
|
+
'default audio': '',
|
|
160
|
+
'default subtitle': '',
|
|
161
|
+
'visual impaired': '',
|
|
162
|
+
'hearing impaired': '',
|
|
163
|
+
'maximum bandwidth': '0',
|
|
164
|
+
'maximum width': '0',
|
|
165
|
+
'maximum height': '0',
|
|
166
|
+
'file extension': 'mkv',
|
|
167
|
+
'external subtitles': 'true',
|
|
168
|
+
'RFC 5646 to ISO 639': 'true',
|
|
169
|
+
'ISO 639': 'alpha_3b',
|
|
170
|
+
'ffmpeg options': '-v error -stats',
|
|
171
|
+
'ffmpeg video codec': 'copy',
|
|
172
|
+
'ffmpeg audio codec': 'copy',
|
|
173
|
+
'overwrite': 'false',
|
|
174
|
+
'never overwrite': 'false' }
|
|
175
|
+
default_config['Headers'] = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:124.0) Gecko/20100101 Firefox/124.0'}
|
|
176
|
+
if not config:
|
|
177
|
+
config = default_config
|
|
178
|
+
config_changed = True
|
|
179
|
+
else:
|
|
180
|
+
if 'Download settings' in config:
|
|
181
|
+
for k in default_config['Download settings']:
|
|
182
|
+
if k in config['Download settings']:
|
|
183
|
+
default_config['Download settings'][k] = config['Download settings'][k]
|
|
184
|
+
else:
|
|
185
|
+
config_changed = True
|
|
186
|
+
else:
|
|
187
|
+
config_changed = True
|
|
188
|
+
if 'Headers' in config:
|
|
189
|
+
if not 'User-Agent' in config['Headers']:
|
|
190
|
+
config_changed = True
|
|
191
|
+
for k in config['Headers']:
|
|
192
|
+
default_config['Headers'][k] = config['Headers'][k]
|
|
193
|
+
else:
|
|
194
|
+
config_changed = True
|
|
195
|
+
if write_config and config_changed:
|
|
196
|
+
if not os.path.exists(config_path):
|
|
197
|
+
os.makedirs(config_path)
|
|
198
|
+
with open(os.path.join(config_path, 'settings.ini'), 'w') as configfile:
|
|
199
|
+
default_config.write(configfile)
|
|
200
|
+
return default_config
|
|
201
|
+
|
|
202
|
+
def main():
|
|
203
|
+
# Read arguments
|
|
204
|
+
parser = argparse.ArgumentParser(description='Download HLS streams.')
|
|
205
|
+
parser.add_argument('url', metavar='URL', help='stream URL')
|
|
206
|
+
parser.add_argument('filename', metavar='OUTPUT', help='output file')
|
|
207
|
+
parser.add_argument('-v', '--verbose', help='print FFmpeg command', action='store_true')
|
|
208
|
+
parser.add_argument('-c', '--config', help='config file', metavar='CONFIG FILE')
|
|
209
|
+
parser.add_argument('-s', '--subonly', help='download subtitles only', action='store_true')
|
|
210
|
+
parser.add_argument('-e', '--extsubs', help='download subtitles to external files', action='store_true')
|
|
211
|
+
parser.add_argument('-m', '--muxsubs', help='mux subtitles to video file', action='store_true')
|
|
212
|
+
parser.add_argument('-a', '--audio', help='audio languages, e.g. \"fin, en\"', metavar='AUDIO LANGUAGES', default=None)
|
|
213
|
+
parser.add_argument('-u', '--subtitles', help='subtitle languages', default=None)
|
|
214
|
+
parser.add_argument('-b', '--begin', help='start live stream from the first segment', action='store_true')
|
|
215
|
+
parser.add_argument('-l', '--live_start_index', help='start live stream from segment', default=None)
|
|
216
|
+
parser.add_argument('-mw', '--max_width', help='maximum video width in pixels', default=None)
|
|
217
|
+
parser.add_argument('-mh', '--max_height', help='maximum video height in pixels', default=None)
|
|
218
|
+
parser.add_argument('-mb', '--max_bitrate', help='maximum video bitrate (bps)', default=None)
|
|
219
|
+
parser.add_argument('-r', '--variant', help='select variant', default=None)
|
|
220
|
+
parser.add_argument('-y', '--overwrite', help='overwrite output files without asking', action='store_true')
|
|
221
|
+
parser.add_argument('-n', '--never', help='never overwrite existing files', action='store_true')
|
|
222
|
+
# parser.add_argument('-f', '--ffmpeg_options', help='ffmpeg general and input options', default=None)
|
|
223
|
+
args = parser.parse_args()
|
|
224
|
+
|
|
225
|
+
# Load config
|
|
226
|
+
if os.name == 'nt':
|
|
227
|
+
config_path = os.path.join(os.environ['APPDATA'], 'viihdexdl')
|
|
228
|
+
elif os.name == 'posix' and sys.platform != 'darwin':
|
|
229
|
+
config_path = os.path.join(os.path.expanduser('~/.config'), 'viihdexdl')
|
|
230
|
+
if not args.config:
|
|
231
|
+
if not os.path.exists(os.path.join(config_path, 'settings.ini')):
|
|
232
|
+
config = create_config(config_path)
|
|
233
|
+
else:
|
|
234
|
+
config = configparser.ConfigParser()
|
|
235
|
+
config.read(os.path.join(config_path, 'settings.ini'))
|
|
236
|
+
config = create_config(config_path, config)
|
|
237
|
+
else:
|
|
238
|
+
config = configparser.ConfigParser()
|
|
239
|
+
config.read(args.config)
|
|
240
|
+
config = create_config(config_path, config, False)
|
|
241
|
+
dl_settings = config['Download settings']
|
|
242
|
+
headers = config['Headers']
|
|
243
|
+
ffmpeg_ua = f' -user_agent "{headers['User-Agent']}"' if headers.get('User-Agent') else ''
|
|
244
|
+
|
|
245
|
+
recording_url = args.url
|
|
246
|
+
filename = args.filename
|
|
247
|
+
sub_only = args.subonly
|
|
248
|
+
ext_subs = args.extsubs if args.extsubs else dl_settings.getboolean('external subtitles')
|
|
249
|
+
if args.muxsubs:
|
|
250
|
+
ext_subs = False
|
|
251
|
+
if dl_settings.getboolean('overwrite') or args.overwrite:
|
|
252
|
+
dl_settings['ffmpeg options'] = dl_settings['ffmpeg options'] + ' ' + '-y'
|
|
253
|
+
if dl_settings.getboolean('never overwrite') or args.never:
|
|
254
|
+
dl_settings['ffmpeg options'] = dl_settings['ffmpeg options'] + ' ' + '-n'
|
|
255
|
+
if args.begin:
|
|
256
|
+
dl_settings['ffmpeg options'] = dl_settings['ffmpeg options'] + ' ' + '-live_start_index 0'
|
|
257
|
+
elif args.live_start_index:
|
|
258
|
+
dl_settings['ffmpeg options'] = dl_settings['ffmpeg options'] + ' ' + '-live_start_index ' + args.live_start_index
|
|
259
|
+
|
|
260
|
+
# if args.ffmpeg_options:
|
|
261
|
+
# dl_settings['ffmpeg_options'] = dl_settings['ffmpeg_options'] + ' ' + args.ffmpeg_options
|
|
262
|
+
|
|
263
|
+
dl_settings['audio languages'] = args.audio if args.audio else dl_settings['audio languages']
|
|
264
|
+
dl_settings['subtitle languages'] = args.subtitles if args.subtitles else dl_settings['subtitle languages']
|
|
265
|
+
to_iso = dl_settings.getboolean('RFC 5646 to ISO 639') if 'RFC 5646 to ISO 639' in dl_settings else True
|
|
266
|
+
iso = dl_settings['ISO 639'] if 'ISO 639' in dl_settings else 'alpha_3b'
|
|
267
|
+
|
|
268
|
+
if filename.endswith('.mkv'):
|
|
269
|
+
dl_settings['file extension'] = 'mkv'
|
|
270
|
+
filename = filename[:-4]
|
|
271
|
+
elif filename.endswith('.mp4'):
|
|
272
|
+
dl_settings['file extension'] = 'mp4'
|
|
273
|
+
filename = filename[:-4]
|
|
274
|
+
|
|
275
|
+
# Download DASH manifest
|
|
276
|
+
get_playlist = requests.get(recording_url, headers = headers)
|
|
277
|
+
|
|
278
|
+
ns = {'mpd':'urn:mpeg:dash:schema:mpd:2011'}
|
|
279
|
+
root = ET.fromstring(get_playlist.text).find('mpd:Period', ns)
|
|
280
|
+
aset = root.findall('mpd:AdaptationSet', ns)
|
|
281
|
+
|
|
282
|
+
# Parse manifest and create FFmpeg command
|
|
283
|
+
recording_url = get_playlist.url
|
|
284
|
+
hls_url = recording_url.rsplit('.mpd', 1)[0].rsplit('/', 1)[0]
|
|
285
|
+
av_audio = []
|
|
286
|
+
audio_dict = {}
|
|
287
|
+
audio_count = {}
|
|
288
|
+
audio_metadata = ''
|
|
289
|
+
av_subs = []
|
|
290
|
+
sub_dict = {}
|
|
291
|
+
wanted_audio = [] if not dl_settings['audio languages'] else [get_language(x.strip()) for x in dl_settings['audio languages'].split(',')]
|
|
292
|
+
wanted_subs = [] if not dl_settings['subtitle languages'] else [get_language(x.strip()) for x in dl_settings['subtitle languages'].split(',')]
|
|
293
|
+
default_audio_list = [] if not dl_settings['default audio'] else [get_language(x.strip()) for x in dl_settings['default audio'].split(',')]
|
|
294
|
+
default_sub_list = [] if not dl_settings['default subtitle'] else [get_language(x.strip()) for x in dl_settings['default subtitle'].split(',')]
|
|
295
|
+
visual_impaired = None if (not dl_settings['visual impaired'] or dl_settings['file extension'] == 'mp4') else get_language(dl_settings['visual impaired'])
|
|
296
|
+
hearing_impaired = None if (not dl_settings['hearing impaired'] or dl_settings['file extension'] == 'mp4') else get_language(dl_settings['hearing impaired'])
|
|
297
|
+
variant = 0
|
|
298
|
+
avar = 0
|
|
299
|
+
for x in ['maximum bandwidth', 'maximum width', 'maximum height']:
|
|
300
|
+
if not dl_settings[x]:
|
|
301
|
+
dl_settings[x] = '0'
|
|
302
|
+
max_bw = 0
|
|
303
|
+
max_dl_bw = int(args.max_bitrate) if args.max_bitrate else int(dl_settings['maximum bandwidth'])
|
|
304
|
+
max_width = 0
|
|
305
|
+
max_dl_width = int(args.max_width) if args.max_width else int(dl_settings['maximum width'])
|
|
306
|
+
max_height = 0
|
|
307
|
+
max_dl_height = int(args.max_height) if args.max_height else int(dl_settings['maximum height'])
|
|
308
|
+
if sub_only:
|
|
309
|
+
cmd = ''
|
|
310
|
+
for x in aset:
|
|
311
|
+
if x.get('contentType') == 'text':
|
|
312
|
+
l = x.get('lang').strip('"')
|
|
313
|
+
lang = get_language(l)
|
|
314
|
+
if not lang_in_langs(lang, av_subs):
|
|
315
|
+
sub_uri = x.find('mpd:SegmentTemplate', ns).get('media')
|
|
316
|
+
sub_uri = sub_uri.replace('$RepresentationID$', x.find('mpd:Representation', ns).get('id'))
|
|
317
|
+
if not (sub_uri.startswith('http://') or sub_uri.startswith('https://')):
|
|
318
|
+
sub_uri = hls_url + '/dash/' + sub_uri
|
|
319
|
+
timeline = x.find('mpd:SegmentTemplate', ns).find('mpd:SegmentTimeline', ns).find('mpd:S', ns).attrib
|
|
320
|
+
av_subs.append(lang)
|
|
321
|
+
sub_dict[lang] = [sub_uri, timeline]
|
|
322
|
+
print(sub_dict)
|
|
323
|
+
if not sub_dict:
|
|
324
|
+
if finnish:
|
|
325
|
+
print('Tekstityksiä ei löytynyt.')
|
|
326
|
+
else:
|
|
327
|
+
print('No subtitles found.')
|
|
328
|
+
else:
|
|
329
|
+
sub = list_tracks(av_subs, wanted_subs, default_sub_list, True)[0][0]
|
|
330
|
+
complete_filename = filename + '.' + get_iso(sub, to_iso, iso) + '.srt'
|
|
331
|
+
if finnish:
|
|
332
|
+
print('Aloitetaan tekstityksen \033[32m' + complete_filename + '\033[39m lataus.\n')
|
|
333
|
+
else:
|
|
334
|
+
print('Starting downloading subtitle \033[32m' + complete_filename + '\033[39m.\n')
|
|
335
|
+
download_sub(complete_filename, sub_dict[sub][0], sub_dict[sub][1])
|
|
336
|
+
|
|
337
|
+
else:
|
|
338
|
+
for a in aset:
|
|
339
|
+
# Parse available variants
|
|
340
|
+
if a.get('contentType') == 'video':
|
|
341
|
+
rep = a.findall('mpd:Representation', ns)
|
|
342
|
+
for x in rep:
|
|
343
|
+
bandwidth = int(x.get('bandwidth'))
|
|
344
|
+
resolution = [int(x.get('width')), int(x.get('height'))]
|
|
345
|
+
# print(bandwidth)
|
|
346
|
+
# print(resolution)
|
|
347
|
+
if (args.variant is not None and variant == int(args.variant) or (not args.variant and
|
|
348
|
+
is_best_variant(bandwidth, resolution[0], resolution[1], max_bw, max_dl_bw, max_dl_width, max_dl_height))):
|
|
349
|
+
best_variant = variant
|
|
350
|
+
try:
|
|
351
|
+
audio_tracks = True
|
|
352
|
+
audio_group = x.split('AUDIO=', 1)[1].split(',', 1)[0]
|
|
353
|
+
except:
|
|
354
|
+
audio_tracks = False
|
|
355
|
+
try:
|
|
356
|
+
sub_tracks = True
|
|
357
|
+
sub_group = x.split('SUBTITLES=', 1)[1].split(',', 1)[0]
|
|
358
|
+
except:
|
|
359
|
+
sub_tracks = False
|
|
360
|
+
max_bw = bandwidth
|
|
361
|
+
max_width = resolution[0]
|
|
362
|
+
max_height = resolution[1]
|
|
363
|
+
variant += 1
|
|
364
|
+
mappings = ' -map 0:v:' + str(best_variant)
|
|
365
|
+
# print(mappings)
|
|
366
|
+
|
|
367
|
+
for x in aset:
|
|
368
|
+
# Parse available audio tracks
|
|
369
|
+
if x.get('contentType') == 'audio':
|
|
370
|
+
try:
|
|
371
|
+
l = x.get('lang')
|
|
372
|
+
lang = get_language(l)
|
|
373
|
+
rep = x.find('mpd:Representation', ns)
|
|
374
|
+
bandwidth = int(rep.get('bandwidth'))
|
|
375
|
+
if not lang_in_langs(lang, av_audio):
|
|
376
|
+
av_audio.append(lang)
|
|
377
|
+
audio_dict[lang] = [avar, bandwidth]
|
|
378
|
+
audio_count[lang] = 1
|
|
379
|
+
else:
|
|
380
|
+
if bandwidth > audio_dict[lang][1]:
|
|
381
|
+
audio_dict[lang] = [avar, bandwidth]
|
|
382
|
+
avar += 1
|
|
383
|
+
except:
|
|
384
|
+
pass
|
|
385
|
+
|
|
386
|
+
# Parse available subtitle tracks
|
|
387
|
+
elif x.get('contentType') == 'text':
|
|
388
|
+
l = x.get('lang')
|
|
389
|
+
lang = get_language(l)
|
|
390
|
+
n = ''
|
|
391
|
+
sub_uri = x.find('mpd:SegmentTemplate', ns).get('media')
|
|
392
|
+
sub_uri = sub_uri.replace('$RepresentationID$', x.find('mpd:Representation', ns).get('id'))
|
|
393
|
+
if not (sub_uri.startswith('http://') or sub_uri.startswith('https://')):
|
|
394
|
+
sub_uri = hls_url + '/dash/' + sub_uri
|
|
395
|
+
timeline = x.find('mpd:SegmentTemplate', ns).find('mpd:SegmentTimeline', ns).find('mpd:S', ns).attrib
|
|
396
|
+
if not lang_in_langs(lang, av_subs):
|
|
397
|
+
av_subs.append(lang)
|
|
398
|
+
sub_dict[lang] = [([sub_uri, timeline], is_hi_sub(n), get_iso(lang, to_iso, iso))]
|
|
399
|
+
else:
|
|
400
|
+
sub_dict[lang].append(([sub_uri, timeline], is_hi_sub(n), get_iso(lang, to_iso, iso)))
|
|
401
|
+
|
|
402
|
+
# Create FFmpeg command
|
|
403
|
+
if not av_audio:
|
|
404
|
+
mappings += ' -map 0:a:0'
|
|
405
|
+
audio_list = None
|
|
406
|
+
else:
|
|
407
|
+
audio_list, default_audio = list_tracks(av_audio, wanted_audio, default_audio_list, True)
|
|
408
|
+
a = 0
|
|
409
|
+
for i, x in enumerate(audio_list):
|
|
410
|
+
mappings += ' -map 0:a:' + str(audio_dict[x][0])
|
|
411
|
+
for j in range(audio_count[x]):
|
|
412
|
+
audio_metadata += ' -metadata:s:a:' + str(i+a) + ' language=' + get_iso(x, to_iso, iso)
|
|
413
|
+
a += 1
|
|
414
|
+
a -= 1
|
|
415
|
+
if x in default_audio:
|
|
416
|
+
audio_metadata += ' -disposition:a:' + str(i+a) + ' default'
|
|
417
|
+
if x == visual_impaired:
|
|
418
|
+
audio_metadata += '+visual_impaired'
|
|
419
|
+
elif x == visual_impaired:
|
|
420
|
+
audio_metadata += ' -disposition:a:' + str(i+a) + ' visual_impaired'
|
|
421
|
+
if av_subs:
|
|
422
|
+
sub_list, default_sub = list_tracks(av_subs, wanted_subs, default_sub_list)
|
|
423
|
+
if ext_subs:
|
|
424
|
+
dl = {}
|
|
425
|
+
dl_hi = {}
|
|
426
|
+
for x in sub_list:
|
|
427
|
+
for y in sub_dict[x]:
|
|
428
|
+
if y[1]:
|
|
429
|
+
if not dl_hi.get(y[2]):
|
|
430
|
+
complete_filename = filename + '.' + y[2] + '.HI.srt'
|
|
431
|
+
dl_hi[y[2]] = 1
|
|
432
|
+
else:
|
|
433
|
+
complete_filename = filename + '.' + y[2] + '.HI.(' + str(dl_hi[y[2]]) + ')' + '.srt'
|
|
434
|
+
dl_hi[y[2]] += 1
|
|
435
|
+
else:
|
|
436
|
+
if not dl.get(y[2]):
|
|
437
|
+
complete_filename = filename + '.' + y[2] + '.srt'
|
|
438
|
+
dl[y[2]] = 1
|
|
439
|
+
else:
|
|
440
|
+
complete_filename = filename + '.' + y[2] + '.(' + str(dl[y[2]]) + ')' + '.srt'
|
|
441
|
+
dl[y[2]] += 1
|
|
442
|
+
if finnish:
|
|
443
|
+
print('Aloitetaan tekstityksen \033[32m' + complete_filename + '\033[39m lataus.\n')
|
|
444
|
+
else:
|
|
445
|
+
print('Starting downloading subtitle \033[32m' + complete_filename + '\033[39m.\n')
|
|
446
|
+
download_sub(complete_filename, y[0][0], y[0][1])
|
|
447
|
+
print()
|
|
448
|
+
else:
|
|
449
|
+
if finnish:
|
|
450
|
+
print('Ainoastaan erilliset tekstitystiedostot ovat tuettuja.')
|
|
451
|
+
else:
|
|
452
|
+
print('Only external subtitles are supported')
|
|
453
|
+
|
|
454
|
+
cmd = ( 'ffmpeg ' + dl_settings['ffmpeg options'] + ffmpeg_ua + ' -i \"' + recording_url + '\"' + mappings
|
|
455
|
+
+ ' -c:v ' + dl_settings['ffmpeg video codec'] + ' -c:a ' + dl_settings['ffmpeg audio codec']
|
|
456
|
+
+ audio_metadata + ' \"' + filename + '.' + dl_settings['file extension'] + '\"' )
|
|
457
|
+
|
|
458
|
+
# Launch FFmpeg
|
|
459
|
+
if finnish:
|
|
460
|
+
print('Aloitetaan tallenteen \033[92m' + filename + '.' + dl_settings['file extension'] + '\033[39m lataus.')
|
|
461
|
+
else:
|
|
462
|
+
print('Starting downloading \033[92m' + filename + '.' + dl_settings['file extension'] + '\033[39m.')
|
|
463
|
+
print(f'Video: {max_bw} bps, {max_width}x{max_height}')
|
|
464
|
+
if not audio_list:
|
|
465
|
+
if finnish:
|
|
466
|
+
print('Ladattava ääniraita: (nimetön)')
|
|
467
|
+
else:
|
|
468
|
+
print('Audio track to download: (undefined)')
|
|
469
|
+
else:
|
|
470
|
+
if finnish:
|
|
471
|
+
print('Ladattavat ääniraidat: ' + ', '.join([get_iso(x, to_iso, iso) for x in audio_list]))
|
|
472
|
+
else:
|
|
473
|
+
print('Audio tracks to download: ' + ', '.join([get_iso(x, to_iso, iso) for x in audio_list]))
|
|
474
|
+
if not ext_subs:
|
|
475
|
+
if not sub_tracks:
|
|
476
|
+
if finnish:
|
|
477
|
+
print('Ei tekstityksiä')
|
|
478
|
+
else:
|
|
479
|
+
print('No subtitles')
|
|
480
|
+
else:
|
|
481
|
+
if finnish:
|
|
482
|
+
print('Ladattavat tekstitysraidat: ' + sub_string[:-2])
|
|
483
|
+
else:
|
|
484
|
+
print('Subtitle tracks to download: ' + sub_string[:-2])
|
|
485
|
+
print()
|
|
486
|
+
if args.verbose:
|
|
487
|
+
print(cmd)
|
|
488
|
+
print()
|
|
489
|
+
Popen(cmd, shell=True).wait()
|
|
490
|
+
|
|
491
|
+
if __name__ == "__main__":
|
|
492
|
+
main()
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: viihdexdash
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Command line tool for downloading Elisa Viihde DASH streams with FFmpeg
|
|
5
|
+
Home-page: https://github.com/Qotscha/viihdexdl
|
|
6
|
+
Author: Qotscha
|
|
7
|
+
Classifier: Development Status :: 3 - Alpha
|
|
8
|
+
Classifier: Environment :: Console
|
|
9
|
+
Classifier: Environment :: Win32 (MS Windows)
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
12
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
Requires-Dist: requests
|
|
17
|
+
Requires-Dist: langcodes
|
|
18
|
+
Requires-Dist: pycaption
|
|
19
|
+
Dynamic: author
|
|
20
|
+
Dynamic: classifier
|
|
21
|
+
Dynamic: description
|
|
22
|
+
Dynamic: description-content-type
|
|
23
|
+
Dynamic: home-page
|
|
24
|
+
Dynamic: license-file
|
|
25
|
+
Dynamic: requires-dist
|
|
26
|
+
Dynamic: summary
|
|
27
|
+
|
|
28
|
+
# ViihdexDASH
|
|
29
|
+
|
|
30
|
+
A command line tool for downloading HLS streams with multiple audio and subtitle tracks (designed especially for Elisa Viihde recordings) using FFmpeg.
|
|
31
|
+
|
|
32
|
+
## Requirements
|
|
33
|
+
[Python 3](https://www.python.org/downloads/) and libraries [langcodes](https://pypi.org/project/langcodes/) and [Requests](https://pypi.org/project/requests/) are required.
|
|
34
|
+
|
|
35
|
+
[FFmpeg](https://www.ffmpeg.org) is needed to download streams.
|
|
36
|
+
|
|
37
|
+
## Usage
|
|
38
|
+
```
|
|
39
|
+
viihdexdash "URL" "FILENAME" [OPTIONS]
|
|
40
|
+
```
|
|
41
|
+
The URL must point to a HLS master playlist.
|
|
42
|
+
|
|
43
|
+
Options include:
|
|
44
|
+
```
|
|
45
|
+
-h, --help show this help message and exit
|
|
46
|
+
-v, --verbose print FFmpeg command
|
|
47
|
+
-c CONFIG FILE, --config CONFIG FILE
|
|
48
|
+
config file
|
|
49
|
+
-s, --subonly download subtitles only
|
|
50
|
+
-e, --extsubs download subtitles to external files
|
|
51
|
+
-m, --muxsubs mux subtitles to video file
|
|
52
|
+
-a AUDIO LANGUAGES, --audio AUDIO LANGUAGES
|
|
53
|
+
audio languages, e.g. "fin, en"
|
|
54
|
+
-u SUBTITLES, --subtitles SUBTITLES
|
|
55
|
+
subtitle languages
|
|
56
|
+
-b, --begin start live stream from the first segment
|
|
57
|
+
-l LIVE_START_INDEX, --live_start_index LIVE_START_INDEX
|
|
58
|
+
start live stream from segment
|
|
59
|
+
-mw MAX_WIDTH, --max_width MAX_WIDTH
|
|
60
|
+
maximum video width in pixels
|
|
61
|
+
-mh MAX_HEIGHT, --max_height MAX_HEIGHT
|
|
62
|
+
maximum video height in pixels
|
|
63
|
+
-mb MAX_BITRATE, --max_bitrate MAX_BITRATE
|
|
64
|
+
maximum video bitrate (bps)
|
|
65
|
+
-r VARIANT, --variant VARIANT
|
|
66
|
+
select variant
|
|
67
|
+
-y, --overwrite overwrite output files without asking
|
|
68
|
+
-n, --never never overwrite existing files
|
|
69
|
+
```
|
|
70
|
+
## Settings
|
|
71
|
+
Download settings are defined in settings.ini which in Windows systems is located in `%APPDATA%\viihdexdl`. Preferred languages are set using two-letter ISO 639-1 or three-letter ISO 639-2 [codes](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes).
|
|
72
|
+
```
|
|
73
|
+
audio languages: Languages of audio tracks to be downloaded.
|
|
74
|
+
If left empty, all available tracks are downloaded.
|
|
75
|
+
subtitle languages: Languages of subtitle tracks to be downloaded.
|
|
76
|
+
If left empty, all available tracks are downloaded.
|
|
77
|
+
default audio: Languages of the audio tracks for which 'default' flag can be set. Preferably
|
|
78
|
+
the track with the language listed first is flagged, but if no track of that
|
|
79
|
+
language is downloaded then the second listed is checked etc.
|
|
80
|
+
default subtitle: Languages of the audio tracks for which 'default' flag can be set.
|
|
81
|
+
visual impaired: Language of the audio track for which 'visual impaired' flag
|
|
82
|
+
is set (only works with .mkv files).
|
|
83
|
+
hearing impaired: Language of the subtitle track for which 'hearing impaired' flag
|
|
84
|
+
is set (onlyworks with .mkv files).
|
|
85
|
+
maximum bandwidth: The variant with the highest bitrate below this limit (bit/s) is downloaded.
|
|
86
|
+
If left empty or set to 0, the best variant is downloaded.
|
|
87
|
+
maximum width: Maximum width (pixels) for video.
|
|
88
|
+
maximum height: Maximum height (pixels) for video.
|
|
89
|
+
file extension: File extension of the output file. Use mkv (preferred) or mp4.
|
|
90
|
+
external subtitles Download subtitles to external files.
|
|
91
|
+
rfc 5646 to iso 639: Convert RFC 5646 language (e.g. nl-NL) tags to ISO 639-1/2 language codes.
|
|
92
|
+
iso 639: Type of language codes to use in metadata.
|
|
93
|
+
Use alpha_2 (nl), alpha_3 (nld) or alpha_3b (dut).
|
|
94
|
+
ffmpeg options: FFmpeg global and input options.
|
|
95
|
+
ffmpeg video codec: FFmpeg video codec options.
|
|
96
|
+
ffmpeg audio codec: FFmpeg audio codec options.
|
|
97
|
+
overwrite: Overwrite output files without asking.
|
|
98
|
+
never overwrite: Never overwrite existing files.
|
|
99
|
+
```
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
viihdexdash/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
viihdexdash/__main__.py,sha256=sPUfqiPQB9l7kK3bUs05HDh6FRHGN92Clv73u6hecQU,23289
|
|
3
|
+
viihdexdash-0.2.0.dist-info/licenses/LICENSE,sha256=GviPYCoqIz9L8HZP_6P0VSlBUr0jLG2kCVrdVkaWeY0,1090
|
|
4
|
+
viihdexdash-0.2.0.dist-info/METADATA,sha256=SbWGN5hy11WumrJPjAeojcESYVRMuKPcqLI2mGCyWPA,4768
|
|
5
|
+
viihdexdash-0.2.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
6
|
+
viihdexdash-0.2.0.dist-info/entry_points.txt,sha256=7sN3pWIJOX3eqa4ziOYUSa1-vXrwKljgbhSsyz0rqyk,58
|
|
7
|
+
viihdexdash-0.2.0.dist-info/top_level.txt,sha256=t0twAqLpc1K9984ixiYRwdJcZGhhPC6kTHfngWuN5zE,12
|
|
8
|
+
viihdexdash-0.2.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2021-2026 Qotscha
|
|
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 @@
|
|
|
1
|
+
viihdexdash
|