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.
File without changes
@@ -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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ viihdexdash = viihdexdash.__main__:main
@@ -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