StreamingCommunity 2.5.2__py3-none-any.whl → 2.5.5__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.
Potentially problematic release.
This version of StreamingCommunity might be problematic. Click here for more details.
- StreamingCommunity/Api/Player/Helper/Vixcloud/js_parser.py +143 -143
- StreamingCommunity/Api/Player/Helper/Vixcloud/util.py +136 -136
- StreamingCommunity/Api/Player/ddl.py +89 -89
- StreamingCommunity/Api/Player/maxstream.py +151 -151
- StreamingCommunity/Api/Player/supervideo.py +193 -193
- StreamingCommunity/Api/Player/vixcloud.py +272 -272
- StreamingCommunity/Api/Site/1337xx/__init__.py +51 -50
- StreamingCommunity/Api/Site/1337xx/costant.py +14 -14
- StreamingCommunity/Api/Site/1337xx/site.py +87 -89
- StreamingCommunity/Api/Site/1337xx/title.py +63 -64
- StreamingCommunity/Api/Site/altadefinizionegratis/__init__.py +74 -50
- StreamingCommunity/Api/Site/altadefinizionegratis/costant.py +21 -19
- StreamingCommunity/Api/Site/altadefinizionegratis/film.py +81 -72
- StreamingCommunity/Api/Site/altadefinizionegratis/site.py +116 -94
- StreamingCommunity/Api/Site/animeunity/__init__.py +75 -50
- StreamingCommunity/Api/Site/animeunity/costant.py +21 -19
- StreamingCommunity/Api/Site/animeunity/film_serie.py +171 -134
- StreamingCommunity/Api/Site/animeunity/site.py +191 -174
- StreamingCommunity/Api/Site/animeunity/util/ScrapeSerie.py +97 -97
- StreamingCommunity/Api/Site/cb01new/__init__.py +51 -51
- StreamingCommunity/Api/Site/cb01new/costant.py +19 -19
- StreamingCommunity/Api/Site/cb01new/film.py +61 -71
- StreamingCommunity/Api/Site/cb01new/site.py +82 -82
- StreamingCommunity/Api/Site/ddlstreamitaly/__init__.py +55 -55
- StreamingCommunity/Api/Site/ddlstreamitaly/costant.py +20 -20
- StreamingCommunity/Api/Site/ddlstreamitaly/series.py +149 -145
- StreamingCommunity/Api/Site/ddlstreamitaly/site.py +98 -98
- StreamingCommunity/Api/Site/ddlstreamitaly/util/ScrapeSerie.py +84 -84
- StreamingCommunity/Api/Site/guardaserie/__init__.py +50 -50
- StreamingCommunity/Api/Site/guardaserie/costant.py +19 -19
- StreamingCommunity/Api/Site/guardaserie/series.py +199 -198
- StreamingCommunity/Api/Site/guardaserie/site.py +89 -89
- StreamingCommunity/Api/Site/guardaserie/util/ScrapeSerie.py +110 -110
- StreamingCommunity/Api/Site/ilcorsaronero/__init__.py +51 -51
- StreamingCommunity/Api/Site/ilcorsaronero/costant.py +18 -18
- StreamingCommunity/Api/Site/ilcorsaronero/site.py +71 -71
- StreamingCommunity/Api/Site/ilcorsaronero/title.py +44 -44
- StreamingCommunity/Api/Site/ilcorsaronero/util/ilCorsarScraper.py +149 -149
- StreamingCommunity/Api/Site/mostraguarda/__init__.py +48 -48
- StreamingCommunity/Api/Site/mostraguarda/costant.py +18 -18
- StreamingCommunity/Api/Site/mostraguarda/film.py +90 -101
- StreamingCommunity/Api/Site/streamingcommunity/__init__.py +79 -55
- StreamingCommunity/Api/Site/streamingcommunity/costant.py +21 -19
- StreamingCommunity/Api/Site/streamingcommunity/film.py +86 -75
- StreamingCommunity/Api/Site/streamingcommunity/series.py +259 -207
- StreamingCommunity/Api/Site/streamingcommunity/site.py +156 -142
- StreamingCommunity/Api/Site/streamingcommunity/util/ScrapeSerie.py +124 -124
- StreamingCommunity/Api/Template/Class/SearchType.py +101 -101
- StreamingCommunity/Api/Template/Util/__init__.py +4 -4
- StreamingCommunity/Api/Template/Util/get_domain.py +201 -201
- StreamingCommunity/Api/Template/Util/manage_ep.py +178 -178
- StreamingCommunity/Api/Template/Util/recall_search.py +37 -37
- StreamingCommunity/Api/Template/__init__.py +2 -2
- StreamingCommunity/Api/Template/site.py +87 -87
- StreamingCommunity/Lib/Downloader/HLS/downloader.py +529 -1008
- StreamingCommunity/Lib/Downloader/HLS/proxyes.py +110 -110
- StreamingCommunity/Lib/Downloader/HLS/segments.py +446 -573
- StreamingCommunity/Lib/Downloader/MP4/downloader.py +181 -155
- StreamingCommunity/Lib/Downloader/TOR/downloader.py +297 -295
- StreamingCommunity/Lib/Downloader/__init__.py +4 -4
- StreamingCommunity/Lib/FFmpeg/__init__.py +4 -4
- StreamingCommunity/Lib/FFmpeg/capture.py +170 -170
- StreamingCommunity/Lib/FFmpeg/command.py +264 -296
- StreamingCommunity/Lib/FFmpeg/util.py +248 -248
- StreamingCommunity/Lib/M3U8/__init__.py +5 -5
- StreamingCommunity/Lib/M3U8/decryptor.py +164 -164
- StreamingCommunity/Lib/M3U8/estimator.py +146 -228
- StreamingCommunity/Lib/M3U8/parser.py +666 -666
- StreamingCommunity/Lib/M3U8/url_fixer.py +57 -57
- StreamingCommunity/Lib/TMBD/__init__.py +1 -1
- StreamingCommunity/Lib/TMBD/obj_tmbd.py +39 -39
- StreamingCommunity/Lib/TMBD/tmdb.py +345 -345
- StreamingCommunity/TelegramHelp/__init__.py +0 -0
- StreamingCommunity/TelegramHelp/request_manager.py +82 -0
- StreamingCommunity/TelegramHelp/session.py +56 -0
- StreamingCommunity/TelegramHelp/telegram_bot.py +561 -0
- StreamingCommunity/Upload/update.py +75 -67
- StreamingCommunity/Upload/version.py +5 -5
- StreamingCommunity/Util/_jsonConfig.py +227 -228
- StreamingCommunity/Util/call_stack.py +42 -42
- StreamingCommunity/Util/color.py +20 -20
- StreamingCommunity/Util/console.py +12 -12
- StreamingCommunity/Util/ffmpeg_installer.py +342 -370
- StreamingCommunity/Util/headers.py +159 -159
- StreamingCommunity/Util/logger.py +61 -61
- StreamingCommunity/Util/message.py +36 -64
- StreamingCommunity/Util/os.py +500 -507
- StreamingCommunity/Util/table.py +271 -228
- StreamingCommunity/run.py +352 -245
- {StreamingCommunity-2.5.2.dist-info → StreamingCommunity-2.5.5.dist-info}/LICENSE +674 -674
- {StreamingCommunity-2.5.2.dist-info → StreamingCommunity-2.5.5.dist-info}/METADATA +601 -543
- StreamingCommunity-2.5.5.dist-info/RECORD +96 -0
- {StreamingCommunity-2.5.2.dist-info → StreamingCommunity-2.5.5.dist-info}/entry_points.txt +0 -1
- StreamingCommunity/Api/Player/Helper/Vixcloud/__pycache__/js_parser.cpython-313.pyc +0 -0
- StreamingCommunity/Api/Player/Helper/Vixcloud/__pycache__/js_parser.cpython-39.pyc +0 -0
- StreamingCommunity/Api/Player/Helper/Vixcloud/__pycache__/util.cpython-313.pyc +0 -0
- StreamingCommunity/Api/Player/Helper/Vixcloud/__pycache__/util.cpython-39.pyc +0 -0
- StreamingCommunity/Api/Player/__pycache__/ddl.cpython-313.pyc +0 -0
- StreamingCommunity/Api/Player/__pycache__/ddl.cpython-39.pyc +0 -0
- StreamingCommunity/Api/Player/__pycache__/maxstream.cpython-313.pyc +0 -0
- StreamingCommunity/Api/Player/__pycache__/maxstream.cpython-39.pyc +0 -0
- StreamingCommunity/Api/Player/__pycache__/supervideo.cpython-313.pyc +0 -0
- StreamingCommunity/Api/Player/__pycache__/supervideo.cpython-39.pyc +0 -0
- StreamingCommunity/Api/Player/__pycache__/vixcloud.cpython-313.pyc +0 -0
- StreamingCommunity/Api/Player/__pycache__/vixcloud.cpython-39.pyc +0 -0
- StreamingCommunity/Api/Site/1337xx/__pycache__/__init__.cpython-313.pyc +0 -0
- StreamingCommunity/Api/Site/1337xx/__pycache__/__init__.cpython-39.pyc +0 -0
- StreamingCommunity/Api/Site/1337xx/__pycache__/costant.cpython-313.pyc +0 -0
- StreamingCommunity/Api/Site/1337xx/__pycache__/costant.cpython-39.pyc +0 -0
- StreamingCommunity/Api/Site/1337xx/__pycache__/site.cpython-313.pyc +0 -0
- StreamingCommunity/Api/Site/1337xx/__pycache__/site.cpython-39.pyc +0 -0
- StreamingCommunity/Api/Site/1337xx/__pycache__/title.cpython-313.pyc +0 -0
- StreamingCommunity/Api/Site/1337xx/__pycache__/title.cpython-39.pyc +0 -0
- StreamingCommunity/Api/Site/altadefinizionegratis/__pycache__/__init__.cpython-313.pyc +0 -0
- StreamingCommunity/Api/Site/altadefinizionegratis/__pycache__/__init__.cpython-39.pyc +0 -0
- StreamingCommunity/Api/Site/altadefinizionegratis/__pycache__/costant.cpython-313.pyc +0 -0
- StreamingCommunity/Api/Site/altadefinizionegratis/__pycache__/costant.cpython-39.pyc +0 -0
- StreamingCommunity/Api/Site/altadefinizionegratis/__pycache__/film.cpython-313.pyc +0 -0
- StreamingCommunity/Api/Site/altadefinizionegratis/__pycache__/film.cpython-39.pyc +0 -0
- StreamingCommunity/Api/Site/altadefinizionegratis/__pycache__/site.cpython-313.pyc +0 -0
- StreamingCommunity/Api/Site/altadefinizionegratis/__pycache__/site.cpython-39.pyc +0 -0
- StreamingCommunity/Api/Site/animeunity/__pycache__/__init__.cpython-313.pyc +0 -0
- StreamingCommunity/Api/Site/animeunity/__pycache__/__init__.cpython-39.pyc +0 -0
- StreamingCommunity/Api/Site/animeunity/__pycache__/costant.cpython-313.pyc +0 -0
- StreamingCommunity/Api/Site/animeunity/__pycache__/costant.cpython-39.pyc +0 -0
- StreamingCommunity/Api/Site/animeunity/__pycache__/film_serie.cpython-313.pyc +0 -0
- StreamingCommunity/Api/Site/animeunity/__pycache__/film_serie.cpython-39.pyc +0 -0
- StreamingCommunity/Api/Site/animeunity/__pycache__/site.cpython-313.pyc +0 -0
- StreamingCommunity/Api/Site/animeunity/__pycache__/site.cpython-39.pyc +0 -0
- StreamingCommunity/Api/Site/animeunity/util/__pycache__/ScrapeSerie.cpython-313.pyc +0 -0
- StreamingCommunity/Api/Site/animeunity/util/__pycache__/ScrapeSerie.cpython-39.pyc +0 -0
- StreamingCommunity/Api/Site/cb01new/__pycache__/__init__.cpython-313.pyc +0 -0
- StreamingCommunity/Api/Site/cb01new/__pycache__/__init__.cpython-39.pyc +0 -0
- StreamingCommunity/Api/Site/cb01new/__pycache__/costant.cpython-313.pyc +0 -0
- StreamingCommunity/Api/Site/cb01new/__pycache__/costant.cpython-39.pyc +0 -0
- StreamingCommunity/Api/Site/cb01new/__pycache__/film.cpython-313.pyc +0 -0
- StreamingCommunity/Api/Site/cb01new/__pycache__/film.cpython-39.pyc +0 -0
- StreamingCommunity/Api/Site/cb01new/__pycache__/site.cpython-313.pyc +0 -0
- StreamingCommunity/Api/Site/cb01new/__pycache__/site.cpython-39.pyc +0 -0
- StreamingCommunity/Api/Site/ddlstreamitaly/__pycache__/__init__.cpython-313.pyc +0 -0
- StreamingCommunity/Api/Site/ddlstreamitaly/__pycache__/__init__.cpython-39.pyc +0 -0
- StreamingCommunity/Api/Site/ddlstreamitaly/__pycache__/costant.cpython-313.pyc +0 -0
- StreamingCommunity/Api/Site/ddlstreamitaly/__pycache__/costant.cpython-39.pyc +0 -0
- StreamingCommunity/Api/Site/ddlstreamitaly/__pycache__/series.cpython-313.pyc +0 -0
- StreamingCommunity/Api/Site/ddlstreamitaly/__pycache__/series.cpython-39.pyc +0 -0
- StreamingCommunity/Api/Site/ddlstreamitaly/__pycache__/site.cpython-313.pyc +0 -0
- StreamingCommunity/Api/Site/ddlstreamitaly/__pycache__/site.cpython-39.pyc +0 -0
- StreamingCommunity/Api/Site/ddlstreamitaly/util/__pycache__/ScrapeSerie.cpython-313.pyc +0 -0
- StreamingCommunity/Api/Site/ddlstreamitaly/util/__pycache__/ScrapeSerie.cpython-39.pyc +0 -0
- StreamingCommunity/Api/Site/guardaserie/__pycache__/__init__.cpython-313.pyc +0 -0
- StreamingCommunity/Api/Site/guardaserie/__pycache__/__init__.cpython-39.pyc +0 -0
- StreamingCommunity/Api/Site/guardaserie/__pycache__/costant.cpython-313.pyc +0 -0
- StreamingCommunity/Api/Site/guardaserie/__pycache__/costant.cpython-39.pyc +0 -0
- StreamingCommunity/Api/Site/guardaserie/__pycache__/series.cpython-313.pyc +0 -0
- StreamingCommunity/Api/Site/guardaserie/__pycache__/series.cpython-39.pyc +0 -0
- StreamingCommunity/Api/Site/guardaserie/__pycache__/site.cpython-313.pyc +0 -0
- StreamingCommunity/Api/Site/guardaserie/__pycache__/site.cpython-39.pyc +0 -0
- StreamingCommunity/Api/Site/guardaserie/util/__pycache__/ScrapeSerie.cpython-313.pyc +0 -0
- StreamingCommunity/Api/Site/guardaserie/util/__pycache__/ScrapeSerie.cpython-39.pyc +0 -0
- StreamingCommunity/Api/Site/ilcorsaronero/__pycache__/__init__.cpython-313.pyc +0 -0
- StreamingCommunity/Api/Site/ilcorsaronero/__pycache__/__init__.cpython-39.pyc +0 -0
- StreamingCommunity/Api/Site/ilcorsaronero/__pycache__/costant.cpython-313.pyc +0 -0
- StreamingCommunity/Api/Site/ilcorsaronero/__pycache__/costant.cpython-39.pyc +0 -0
- StreamingCommunity/Api/Site/ilcorsaronero/__pycache__/site.cpython-313.pyc +0 -0
- StreamingCommunity/Api/Site/ilcorsaronero/__pycache__/site.cpython-39.pyc +0 -0
- StreamingCommunity/Api/Site/ilcorsaronero/__pycache__/title.cpython-313.pyc +0 -0
- StreamingCommunity/Api/Site/ilcorsaronero/__pycache__/title.cpython-39.pyc +0 -0
- StreamingCommunity/Api/Site/ilcorsaronero/util/__pycache__/ilCorsarScraper.cpython-313.pyc +0 -0
- StreamingCommunity/Api/Site/ilcorsaronero/util/__pycache__/ilCorsarScraper.cpython-39.pyc +0 -0
- StreamingCommunity/Api/Site/mostraguarda/__pycache__/__init__.cpython-313.pyc +0 -0
- StreamingCommunity/Api/Site/mostraguarda/__pycache__/__init__.cpython-39.pyc +0 -0
- StreamingCommunity/Api/Site/mostraguarda/__pycache__/costant.cpython-313.pyc +0 -0
- StreamingCommunity/Api/Site/mostraguarda/__pycache__/costant.cpython-39.pyc +0 -0
- StreamingCommunity/Api/Site/mostraguarda/__pycache__/film.cpython-313.pyc +0 -0
- StreamingCommunity/Api/Site/mostraguarda/__pycache__/film.cpython-39.pyc +0 -0
- StreamingCommunity/Api/Site/streamingcommunity/__pycache__/__init__.cpython-313.pyc +0 -0
- StreamingCommunity/Api/Site/streamingcommunity/__pycache__/__init__.cpython-39.pyc +0 -0
- StreamingCommunity/Api/Site/streamingcommunity/__pycache__/costant.cpython-313.pyc +0 -0
- StreamingCommunity/Api/Site/streamingcommunity/__pycache__/costant.cpython-39.pyc +0 -0
- StreamingCommunity/Api/Site/streamingcommunity/__pycache__/film.cpython-313.pyc +0 -0
- StreamingCommunity/Api/Site/streamingcommunity/__pycache__/film.cpython-39.pyc +0 -0
- StreamingCommunity/Api/Site/streamingcommunity/__pycache__/series.cpython-313.pyc +0 -0
- StreamingCommunity/Api/Site/streamingcommunity/__pycache__/series.cpython-39.pyc +0 -0
- StreamingCommunity/Api/Site/streamingcommunity/__pycache__/site.cpython-313.pyc +0 -0
- StreamingCommunity/Api/Site/streamingcommunity/__pycache__/site.cpython-39.pyc +0 -0
- StreamingCommunity/Api/Site/streamingcommunity/util/__pycache__/ScrapeSerie.cpython-313.pyc +0 -0
- StreamingCommunity/Api/Site/streamingcommunity/util/__pycache__/ScrapeSerie.cpython-39.pyc +0 -0
- StreamingCommunity/Api/Template/Class/__pycache__/SearchType.cpython-313.pyc +0 -0
- StreamingCommunity/Api/Template/Class/__pycache__/SearchType.cpython-39.pyc +0 -0
- StreamingCommunity/Api/Template/Util/__pycache__/__init__.cpython-313.pyc +0 -0
- StreamingCommunity/Api/Template/Util/__pycache__/__init__.cpython-39.pyc +0 -0
- StreamingCommunity/Api/Template/Util/__pycache__/get_domain.cpython-313.pyc +0 -0
- StreamingCommunity/Api/Template/Util/__pycache__/get_domain.cpython-39.pyc +0 -0
- StreamingCommunity/Api/Template/Util/__pycache__/manage_ep.cpython-313.pyc +0 -0
- StreamingCommunity/Api/Template/Util/__pycache__/manage_ep.cpython-39.pyc +0 -0
- StreamingCommunity/Api/Template/Util/__pycache__/recall_search.cpython-313.pyc +0 -0
- StreamingCommunity/Api/Template/Util/__pycache__/recall_search.cpython-39.pyc +0 -0
- StreamingCommunity/Api/Template/__pycache__/__init__.cpython-313.pyc +0 -0
- StreamingCommunity/Api/Template/__pycache__/__init__.cpython-39.pyc +0 -0
- StreamingCommunity/Api/Template/__pycache__/site.cpython-313.pyc +0 -0
- StreamingCommunity/Api/Template/__pycache__/site.cpython-39.pyc +0 -0
- StreamingCommunity/Lib/Downloader/HLS/__pycache__/downloader.cpython-313.pyc +0 -0
- StreamingCommunity/Lib/Downloader/HLS/__pycache__/downloader.cpython-39.pyc +0 -0
- StreamingCommunity/Lib/Downloader/HLS/__pycache__/proxyes.cpython-313.pyc +0 -0
- StreamingCommunity/Lib/Downloader/HLS/__pycache__/proxyes.cpython-39.pyc +0 -0
- StreamingCommunity/Lib/Downloader/HLS/__pycache__/segments.cpython-313.pyc +0 -0
- StreamingCommunity/Lib/Downloader/HLS/__pycache__/segments.cpython-39.pyc +0 -0
- StreamingCommunity/Lib/Downloader/MP4/__pycache__/downloader.cpython-313.pyc +0 -0
- StreamingCommunity/Lib/Downloader/MP4/__pycache__/downloader.cpython-39.pyc +0 -0
- StreamingCommunity/Lib/Downloader/TOR/__pycache__/downloader.cpython-313.pyc +0 -0
- StreamingCommunity/Lib/Downloader/TOR/__pycache__/downloader.cpython-39.pyc +0 -0
- StreamingCommunity/Lib/Downloader/__pycache__/__init__.cpython-313.pyc +0 -0
- StreamingCommunity/Lib/Downloader/__pycache__/__init__.cpython-39.pyc +0 -0
- StreamingCommunity/Lib/FFmpeg/__pycache__/__init__.cpython-313.pyc +0 -0
- StreamingCommunity/Lib/FFmpeg/__pycache__/__init__.cpython-39.pyc +0 -0
- StreamingCommunity/Lib/FFmpeg/__pycache__/capture.cpython-313.pyc +0 -0
- StreamingCommunity/Lib/FFmpeg/__pycache__/capture.cpython-39.pyc +0 -0
- StreamingCommunity/Lib/FFmpeg/__pycache__/command.cpython-313.pyc +0 -0
- StreamingCommunity/Lib/FFmpeg/__pycache__/command.cpython-39.pyc +0 -0
- StreamingCommunity/Lib/FFmpeg/__pycache__/util.cpython-313.pyc +0 -0
- StreamingCommunity/Lib/FFmpeg/__pycache__/util.cpython-39.pyc +0 -0
- StreamingCommunity/Lib/M3U8/__pycache__/__init__.cpython-313.pyc +0 -0
- StreamingCommunity/Lib/M3U8/__pycache__/__init__.cpython-39.pyc +0 -0
- StreamingCommunity/Lib/M3U8/__pycache__/decryptor.cpython-313.pyc +0 -0
- StreamingCommunity/Lib/M3U8/__pycache__/decryptor.cpython-39.pyc +0 -0
- StreamingCommunity/Lib/M3U8/__pycache__/estimator.cpython-313.pyc +0 -0
- StreamingCommunity/Lib/M3U8/__pycache__/estimator.cpython-39.pyc +0 -0
- StreamingCommunity/Lib/M3U8/__pycache__/parser.cpython-313.pyc +0 -0
- StreamingCommunity/Lib/M3U8/__pycache__/parser.cpython-39.pyc +0 -0
- StreamingCommunity/Lib/M3U8/__pycache__/url_fixer.cpython-313.pyc +0 -0
- StreamingCommunity/Lib/M3U8/__pycache__/url_fixer.cpython-39.pyc +0 -0
- StreamingCommunity/Lib/TMBD/__pycache__/__init__.cpython-313.pyc +0 -0
- StreamingCommunity/Lib/TMBD/__pycache__/__init__.cpython-39.pyc +0 -0
- StreamingCommunity/Lib/TMBD/__pycache__/obj_tmbd.cpython-313.pyc +0 -0
- StreamingCommunity/Lib/TMBD/__pycache__/obj_tmbd.cpython-39.pyc +0 -0
- StreamingCommunity/Lib/TMBD/__pycache__/tmdb.cpython-313.pyc +0 -0
- StreamingCommunity/Lib/TMBD/__pycache__/tmdb.cpython-39.pyc +0 -0
- StreamingCommunity/Upload/__pycache__/update.cpython-313.pyc +0 -0
- StreamingCommunity/Upload/__pycache__/update.cpython-39.pyc +0 -0
- StreamingCommunity/Upload/__pycache__/version.cpython-313.pyc +0 -0
- StreamingCommunity/Upload/__pycache__/version.cpython-39.pyc +0 -0
- StreamingCommunity/Util/__pycache__/_jsonConfig.cpython-313.pyc +0 -0
- StreamingCommunity/Util/__pycache__/_jsonConfig.cpython-39.pyc +0 -0
- StreamingCommunity/Util/__pycache__/call_stack.cpython-313.pyc +0 -0
- StreamingCommunity/Util/__pycache__/call_stack.cpython-39.pyc +0 -0
- StreamingCommunity/Util/__pycache__/color.cpython-313.pyc +0 -0
- StreamingCommunity/Util/__pycache__/color.cpython-39.pyc +0 -0
- StreamingCommunity/Util/__pycache__/console.cpython-313.pyc +0 -0
- StreamingCommunity/Util/__pycache__/console.cpython-39.pyc +0 -0
- StreamingCommunity/Util/__pycache__/ffmpeg_installer.cpython-313.pyc +0 -0
- StreamingCommunity/Util/__pycache__/ffmpeg_installer.cpython-39.pyc +0 -0
- StreamingCommunity/Util/__pycache__/headers.cpython-313.pyc +0 -0
- StreamingCommunity/Util/__pycache__/headers.cpython-39.pyc +0 -0
- StreamingCommunity/Util/__pycache__/logger.cpython-313.pyc +0 -0
- StreamingCommunity/Util/__pycache__/logger.cpython-39.pyc +0 -0
- StreamingCommunity/Util/__pycache__/message.cpython-313.pyc +0 -0
- StreamingCommunity/Util/__pycache__/message.cpython-39.pyc +0 -0
- StreamingCommunity/Util/__pycache__/os.cpython-313.pyc +0 -0
- StreamingCommunity/Util/__pycache__/os.cpython-39.pyc +0 -0
- StreamingCommunity/Util/__pycache__/table.cpython-313.pyc +0 -0
- StreamingCommunity/Util/__pycache__/table.cpython-39.pyc +0 -0
- StreamingCommunity/__pycache__/__init__.cpython-313.pyc +0 -0
- StreamingCommunity/__pycache__/__init__.cpython-39.pyc +0 -0
- StreamingCommunity/__pycache__/run.cpython-313.pyc +0 -0
- StreamingCommunity/__pycache__/run.cpython-39.pyc +0 -0
- StreamingCommunity-2.5.2.dist-info/RECORD +0 -264
- {StreamingCommunity-2.5.2.dist-info → StreamingCommunity-2.5.5.dist-info}/WHEEL +0 -0
- {StreamingCommunity-2.5.2.dist-info → StreamingCommunity-2.5.5.dist-info}/top_level.txt +0 -0
|
@@ -1,1008 +1,529 @@
|
|
|
1
|
-
# 17.10.24
|
|
2
|
-
|
|
3
|
-
import os
|
|
4
|
-
import
|
|
5
|
-
import time
|
|
6
|
-
import logging
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
from StreamingCommunity.Util.
|
|
17
|
-
from StreamingCommunity.Util.
|
|
18
|
-
from StreamingCommunity.Util.
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
""
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
"""
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
if
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
self.
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
'
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
def __init__(self, path_manager):
|
|
531
|
-
"""
|
|
532
|
-
Initializes the ContentJoiner class.
|
|
533
|
-
|
|
534
|
-
Args:
|
|
535
|
-
path_manager (PathManager): An instance of PathManager to manage output paths.
|
|
536
|
-
"""
|
|
537
|
-
self.path_manager: PathManager = path_manager
|
|
538
|
-
|
|
539
|
-
def setup(self, downloaded_video, downloaded_audio, downloaded_subtitle, codec = None):
|
|
540
|
-
"""
|
|
541
|
-
Sets up the content joiner with downloaded media files.
|
|
542
|
-
|
|
543
|
-
Args:
|
|
544
|
-
downloaded_video (list): List of downloaded video information.
|
|
545
|
-
downloaded_audio (list): List of downloaded audio information.
|
|
546
|
-
downloaded_subtitle (list): List of downloaded subtitle information.
|
|
547
|
-
"""
|
|
548
|
-
self.downloaded_video = downloaded_video
|
|
549
|
-
self.downloaded_audio = downloaded_audio
|
|
550
|
-
self.downloaded_subtitle = downloaded_subtitle
|
|
551
|
-
self.codec = codec
|
|
552
|
-
|
|
553
|
-
# Initialize flags to check if media is available
|
|
554
|
-
self.converted_out_path = None
|
|
555
|
-
self.there_is_video = len(downloaded_video) > 0
|
|
556
|
-
self.there_is_audio = len(downloaded_audio) > 0
|
|
557
|
-
self.there_is_subtitle = len(downloaded_subtitle) > 0
|
|
558
|
-
|
|
559
|
-
# Start the joining process
|
|
560
|
-
self.conversione()
|
|
561
|
-
|
|
562
|
-
def conversione(self):
|
|
563
|
-
"""
|
|
564
|
-
Handles the joining of video, audio, and subtitles based on availability.
|
|
565
|
-
"""
|
|
566
|
-
|
|
567
|
-
# Join audio and video if audio is available
|
|
568
|
-
if self.there_is_audio:
|
|
569
|
-
if MERGE_AUDIO:
|
|
570
|
-
|
|
571
|
-
# Join video with audio tracks
|
|
572
|
-
self.converted_out_path = self._join_video_audio()
|
|
573
|
-
|
|
574
|
-
else:
|
|
575
|
-
|
|
576
|
-
# Process each available audio track
|
|
577
|
-
for obj_audio in self.downloaded_audio:
|
|
578
|
-
language = obj_audio.get('language')
|
|
579
|
-
path = obj_audio.get('path')
|
|
580
|
-
|
|
581
|
-
# Set the new path for regular audio
|
|
582
|
-
new_path = self.path_manager.output_filename.replace(".mp4", f"_{language}.mp4")
|
|
583
|
-
|
|
584
|
-
try:
|
|
585
|
-
|
|
586
|
-
# Rename the audio file to the new path
|
|
587
|
-
os.rename(path, new_path)
|
|
588
|
-
logging.info(f"Audio moved to {new_path}")
|
|
589
|
-
|
|
590
|
-
except Exception as e:
|
|
591
|
-
logging.error(f"Failed to move audio {path} to {new_path}: {e}")
|
|
592
|
-
|
|
593
|
-
# Convert video if available
|
|
594
|
-
if self.there_is_video:
|
|
595
|
-
self.converted_out_path = self._join_video()
|
|
596
|
-
|
|
597
|
-
# If no audio but video is available, join video
|
|
598
|
-
else:
|
|
599
|
-
if self.there_is_video:
|
|
600
|
-
self.converted_out_path = self._join_video()
|
|
601
|
-
|
|
602
|
-
# Join subtitles if available
|
|
603
|
-
if self.there_is_subtitle:
|
|
604
|
-
if MERGE_SUBTITLE:
|
|
605
|
-
if self.converted_out_path is not None:
|
|
606
|
-
self.converted_out_path = self._join_video_subtitles(self.converted_out_path)
|
|
607
|
-
|
|
608
|
-
else:
|
|
609
|
-
|
|
610
|
-
# Process each available subtitle track
|
|
611
|
-
for obj_sub in self.downloaded_subtitle:
|
|
612
|
-
language = obj_sub.get('language')
|
|
613
|
-
path = obj_sub.get('path')
|
|
614
|
-
forced = 'forced' in language
|
|
615
|
-
|
|
616
|
-
# Adjust the language name and set the new path based on forced status
|
|
617
|
-
if forced:
|
|
618
|
-
language = language.replace("forced-", "")
|
|
619
|
-
new_path = self.path_manager.output_filename.replace(".mp4", f".{language}.forced.vtt")
|
|
620
|
-
else:
|
|
621
|
-
new_path = self.path_manager.output_filename.replace(".mp4", f".{language}.vtt")
|
|
622
|
-
|
|
623
|
-
try:
|
|
624
|
-
# Rename the subtitle file to the new path
|
|
625
|
-
os.rename(path, new_path)
|
|
626
|
-
logging.info(f"Subtitle moved to {new_path}")
|
|
627
|
-
|
|
628
|
-
except Exception as e:
|
|
629
|
-
logging.error(f"Failed to move subtitle {path} to {new_path}: {e}")
|
|
630
|
-
|
|
631
|
-
def _join_video(self):
|
|
632
|
-
"""
|
|
633
|
-
Joins video segments into a single video file.
|
|
634
|
-
|
|
635
|
-
Returns:
|
|
636
|
-
str: The path to the joined video file.
|
|
637
|
-
"""
|
|
638
|
-
path_join_video = os.path.join(self.path_manager.base_path, "v_v.mp4")
|
|
639
|
-
logging.info(f"JOIN video path: {path_join_video}")
|
|
640
|
-
|
|
641
|
-
# Check if the joined video file already exists
|
|
642
|
-
if not os.path.exists(path_join_video):
|
|
643
|
-
|
|
644
|
-
# Join the video segments into a single video file
|
|
645
|
-
join_video(
|
|
646
|
-
video_path=self.downloaded_video[0].get('path'),
|
|
647
|
-
out_path=path_join_video,
|
|
648
|
-
codec=self.codec
|
|
649
|
-
)
|
|
650
|
-
|
|
651
|
-
else:
|
|
652
|
-
console.log("[red]Output join video already exists.")
|
|
653
|
-
|
|
654
|
-
return path_join_video
|
|
655
|
-
|
|
656
|
-
def _join_video_audio(self):
|
|
657
|
-
"""
|
|
658
|
-
Joins video segments with audio tracks into a single video with audio file.
|
|
659
|
-
|
|
660
|
-
Returns:
|
|
661
|
-
str: The path to the joined video with audio file.
|
|
662
|
-
"""
|
|
663
|
-
path_join_video_audio = os.path.join(self.path_manager.base_path, "v_a.mp4")
|
|
664
|
-
logging.info(f"JOIN audio path: {path_join_video_audio}")
|
|
665
|
-
|
|
666
|
-
# Check if the joined video with audio file already exists
|
|
667
|
-
if not os.path.exists(path_join_video_audio):
|
|
668
|
-
|
|
669
|
-
# Set codec to None if not defined in class
|
|
670
|
-
#if not hasattr(self, 'codec'):
|
|
671
|
-
# self.codec = None
|
|
672
|
-
|
|
673
|
-
# Join the video with audio segments
|
|
674
|
-
join_audios(
|
|
675
|
-
video_path=self.downloaded_video[0].get('path'),
|
|
676
|
-
audio_tracks=self.downloaded_audio,
|
|
677
|
-
out_path=path_join_video_audio,
|
|
678
|
-
codec=self.codec
|
|
679
|
-
)
|
|
680
|
-
|
|
681
|
-
else:
|
|
682
|
-
console.log("[red]Output join video and audio already exists.")
|
|
683
|
-
|
|
684
|
-
return path_join_video_audio
|
|
685
|
-
|
|
686
|
-
def _join_video_subtitles(self, input_path):
|
|
687
|
-
"""
|
|
688
|
-
Joins subtitles with the video.
|
|
689
|
-
|
|
690
|
-
Args:
|
|
691
|
-
input_path (str): The path to the video file to which subtitles will be added.
|
|
692
|
-
|
|
693
|
-
Returns:
|
|
694
|
-
str: The path to the video with subtitles file.
|
|
695
|
-
"""
|
|
696
|
-
path_join_video_subtitle = os.path.join(self.path_manager.base_path, "v_s.mp4")
|
|
697
|
-
logging.info(f"JOIN subtitle path: {path_join_video_subtitle}")
|
|
698
|
-
|
|
699
|
-
# Check if the video with subtitles file already exists
|
|
700
|
-
if not os.path.exists(path_join_video_subtitle):
|
|
701
|
-
|
|
702
|
-
# Join the video with subtitles
|
|
703
|
-
join_subtitle(
|
|
704
|
-
input_path,
|
|
705
|
-
self.downloaded_subtitle,
|
|
706
|
-
path_join_video_subtitle
|
|
707
|
-
)
|
|
708
|
-
|
|
709
|
-
return path_join_video_subtitle
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
class HLS_Downloader:
|
|
713
|
-
def __init__(self, output_filename: str=None, m3u8_playlist: str=None, m3u8_index: str=None, is_playlist_url: bool=True, is_index_url: bool=True):
|
|
714
|
-
"""
|
|
715
|
-
Initializes the HLS_Downloader class.
|
|
716
|
-
|
|
717
|
-
Args:
|
|
718
|
-
output_filename (str): The desired output filename for the downloaded content.
|
|
719
|
-
m3u8_playlist (str): The URL or content of the m3u8 playlist.
|
|
720
|
-
m3u8_index (str): The index URL for m3u8 streams.
|
|
721
|
-
is_playlist_url (bool): Flag indicating if the m3u8_playlist is a URL.
|
|
722
|
-
is_index_url (bool): Flag indicating if the m3u8_index is a URL.
|
|
723
|
-
"""
|
|
724
|
-
if ((m3u8_playlist == None or m3u8_playlist == "") and output_filename is None) or ((m3u8_index == None or m3u8_index == "") and output_filename is None):
|
|
725
|
-
logging.info(f"class 'HLS_Downloader'; call __init__(); no parameter")
|
|
726
|
-
sys.exit(0)
|
|
727
|
-
|
|
728
|
-
self.output_filename = self._generate_output_filename(output_filename, m3u8_playlist, m3u8_index)
|
|
729
|
-
self.path_manager = PathManager(self.output_filename)
|
|
730
|
-
self.download_tracker = DownloadTracker(self.path_manager)
|
|
731
|
-
self.content_extractor = ContentExtractor()
|
|
732
|
-
self.content_downloader = ContentDownloader()
|
|
733
|
-
self.content_joiner = ContentJoiner(self.path_manager)
|
|
734
|
-
|
|
735
|
-
self.m3u8_playlist = m3u8_playlist
|
|
736
|
-
self.m3u8_index = m3u8_index
|
|
737
|
-
self.is_playlist_url = is_playlist_url
|
|
738
|
-
self.is_index_url = is_index_url
|
|
739
|
-
self.expected_real_time = None
|
|
740
|
-
self.instace_parserClass = M3U8_Parser()
|
|
741
|
-
|
|
742
|
-
self.request_m3u8_playlist = None
|
|
743
|
-
self.request_m3u8_index = None
|
|
744
|
-
if (m3u8_playlist == None or m3u8_playlist == ""):
|
|
745
|
-
self.request_m3u8_index = HttpClient().get(self.m3u8_index)
|
|
746
|
-
if (m3u8_index == None or m3u8_index == ""):
|
|
747
|
-
self.request_m3u8_playlist = HttpClient().get(self.m3u8_playlist)
|
|
748
|
-
|
|
749
|
-
def _generate_output_filename(self, output_filename, m3u8_playlist, m3u8_index):
|
|
750
|
-
"""
|
|
751
|
-
Generates a valid output filename based on provided parameters.
|
|
752
|
-
|
|
753
|
-
Args:
|
|
754
|
-
output_filename (str): The desired output filename.
|
|
755
|
-
m3u8_playlist (str): The m3u8 playlist URL or content.
|
|
756
|
-
m3u8_index (str): The m3u8 index URL.
|
|
757
|
-
|
|
758
|
-
Returns:
|
|
759
|
-
str: The generated output filename.
|
|
760
|
-
"""
|
|
761
|
-
root_path = config_manager.get('DEFAULT', 'root_path')
|
|
762
|
-
new_filename = None
|
|
763
|
-
new_folder = os.path.join(root_path, "undefined")
|
|
764
|
-
logging.info(f"class 'HLS_Downloader'; call _generate_output_filename(); destination folder: {new_folder}")
|
|
765
|
-
|
|
766
|
-
# Auto-generate output file name if not present
|
|
767
|
-
if (output_filename is None) or ("mp4" not in output_filename):
|
|
768
|
-
if m3u8_playlist is not None:
|
|
769
|
-
new_filename = os.path.join(new_folder, compute_sha1_hash(m3u8_playlist) + ".mp4")
|
|
770
|
-
else:
|
|
771
|
-
new_filename = os.path.join(new_folder, compute_sha1_hash(m3u8_index) + ".mp4")
|
|
772
|
-
|
|
773
|
-
else:
|
|
774
|
-
|
|
775
|
-
# Check if output_filename contains a folder path
|
|
776
|
-
folder, base_name = os.path.split(output_filename)
|
|
777
|
-
|
|
778
|
-
# If no folder is specified, default to 'undefined'
|
|
779
|
-
if not folder:
|
|
780
|
-
folder = new_folder
|
|
781
|
-
|
|
782
|
-
# Sanitize base name and folder
|
|
783
|
-
folder = os_manager.get_sanitize_path(folder)
|
|
784
|
-
base_name = os_manager.get_sanitize_file(base_name)
|
|
785
|
-
os_manager.create_path(folder)
|
|
786
|
-
|
|
787
|
-
# Parse to only ASCII for compatibility across platforms
|
|
788
|
-
new_filename = os.path.join(folder, base_name)
|
|
789
|
-
|
|
790
|
-
logging.info(f"class 'HLS_Downloader'; call _generate_output_filename(); return path: {new_filename}")
|
|
791
|
-
return new_filename
|
|
792
|
-
|
|
793
|
-
def start(self):
|
|
794
|
-
"""
|
|
795
|
-
Initiates the downloading process. Checks if the output file already exists and proceeds with processing the playlist or index.
|
|
796
|
-
"""
|
|
797
|
-
if os.path.exists(self.output_filename):
|
|
798
|
-
console.log("[red]Output file already exists.")
|
|
799
|
-
return 400
|
|
800
|
-
|
|
801
|
-
self.path_manager.create_directories()
|
|
802
|
-
|
|
803
|
-
# Determine whether to process a playlist or index
|
|
804
|
-
if self.m3u8_playlist:
|
|
805
|
-
if self.m3u8_playlist is not None:
|
|
806
|
-
if self.request_m3u8_playlist != 404:
|
|
807
|
-
logging.info(f"class 'HLS_Downloader'; call start(); parse m3u8 data")
|
|
808
|
-
|
|
809
|
-
self.instace_parserClass.parse_data(uri=self.m3u8_playlist, raw_content=self.request_m3u8_playlist)
|
|
810
|
-
is_masterPlaylist = self.instace_parserClass.is_master_playlist
|
|
811
|
-
|
|
812
|
-
# Check if it's a real master playlist
|
|
813
|
-
if is_masterPlaylist:
|
|
814
|
-
if not GET_ONLY_LINK:
|
|
815
|
-
r_proc = self._process_playlist()
|
|
816
|
-
|
|
817
|
-
if r_proc == 404:
|
|
818
|
-
return 404
|
|
819
|
-
else:
|
|
820
|
-
return None
|
|
821
|
-
|
|
822
|
-
else:
|
|
823
|
-
return {
|
|
824
|
-
'path': self.output_filename,
|
|
825
|
-
'url': self.m3u8_playlist
|
|
826
|
-
}
|
|
827
|
-
|
|
828
|
-
else:
|
|
829
|
-
console.log("[red]Error: URL passed to M3U8_Parser is an index playlist; expected a master playlist. Crucimorfo strikes again!")
|
|
830
|
-
else:
|
|
831
|
-
console.log(f"[red]Error: m3u8_playlist failed request for: {self.m3u8_playlist}")
|
|
832
|
-
else:
|
|
833
|
-
console.log("[red]Error: m3u8_playlist is None")
|
|
834
|
-
|
|
835
|
-
elif self.m3u8_index:
|
|
836
|
-
if self.m3u8_index is not None:
|
|
837
|
-
if self.request_m3u8_index != 404:
|
|
838
|
-
logging.info(f"class 'HLS_Downloader'; call start(); parse m3u8 data")
|
|
839
|
-
|
|
840
|
-
self.instace_parserClass.parse_data(uri=self.m3u8_index, raw_content=self.request_m3u8_index)
|
|
841
|
-
is_masterPlaylist = self.instace_parserClass.is_master_playlist
|
|
842
|
-
|
|
843
|
-
# Check if it's a real index playlist
|
|
844
|
-
if not is_masterPlaylist:
|
|
845
|
-
if not GET_ONLY_LINK:
|
|
846
|
-
self._process_index()
|
|
847
|
-
return None
|
|
848
|
-
|
|
849
|
-
else:
|
|
850
|
-
return {
|
|
851
|
-
'path': self.output_filename,
|
|
852
|
-
'url': self.m3u8_index
|
|
853
|
-
}
|
|
854
|
-
|
|
855
|
-
else:
|
|
856
|
-
console.log("[red]Error: URL passed to M3U8_Parser is an master playlist; expected a index playlist. Crucimorfo strikes again!")
|
|
857
|
-
else:
|
|
858
|
-
console.log("[red]Error: m3u8_index failed request")
|
|
859
|
-
else:
|
|
860
|
-
console.log("[red]Error: m3u8_index is None")
|
|
861
|
-
|
|
862
|
-
# Reset
|
|
863
|
-
self._reset()
|
|
864
|
-
|
|
865
|
-
def _clean(self, out_path: str) -> None:
|
|
866
|
-
"""
|
|
867
|
-
Cleans up temporary files and folders after downloading and processing.
|
|
868
|
-
|
|
869
|
-
Args:
|
|
870
|
-
out_path (str): The path of the output file to be cleaned up.
|
|
871
|
-
"""
|
|
872
|
-
|
|
873
|
-
# Check if the final output file exists
|
|
874
|
-
logging.info(f"Check if end file converted exists: {out_path}")
|
|
875
|
-
if out_path is None or not os.path.isfile(out_path):
|
|
876
|
-
logging.error("Video file converted does not exist.")
|
|
877
|
-
sys.exit(0)
|
|
878
|
-
|
|
879
|
-
# Rename the output file to the desired output filename if it does not already exist
|
|
880
|
-
if not os.path.exists(self.output_filename):
|
|
881
|
-
missing_ts = False
|
|
882
|
-
missing_info = ""
|
|
883
|
-
|
|
884
|
-
# Rename the converted file to the specified output filename
|
|
885
|
-
os.rename(out_path, self.output_filename)
|
|
886
|
-
|
|
887
|
-
# Calculate file size and duration for reporting
|
|
888
|
-
formatted_size = internet_manager.format_file_size(os.path.getsize(self.output_filename))
|
|
889
|
-
formatted_duration = print_duration_table(self.output_filename, description=False, return_string=True)
|
|
890
|
-
|
|
891
|
-
# Collect info about type missing
|
|
892
|
-
for item in list_MissingTs:
|
|
893
|
-
if int(item['nFailed']) >= 1:
|
|
894
|
-
missing_ts = True
|
|
895
|
-
missing_info += f"[red]TS Failed: {item['nFailed']} {item['type']} tracks[/red]\n"
|
|
896
|
-
|
|
897
|
-
# Prepare the report panel content
|
|
898
|
-
print("")
|
|
899
|
-
panel_content = (
|
|
900
|
-
f"[bold green]Download completed![/bold green]\n"
|
|
901
|
-
f"[cyan]File size: [bold red]{formatted_size}[/bold red]\n"
|
|
902
|
-
f"[cyan]Duration: [bold]{formatted_duration}[/bold]\n"
|
|
903
|
-
f"[cyan]Output: [bold]{os.path.abspath(self.output_filename)}[/bold]"
|
|
904
|
-
)
|
|
905
|
-
|
|
906
|
-
if missing_ts:
|
|
907
|
-
panel_content += f"\n{missing_info}"
|
|
908
|
-
|
|
909
|
-
# Display the download completion message
|
|
910
|
-
console.print(Panel(
|
|
911
|
-
panel_content,
|
|
912
|
-
title=f"{os.path.basename(self.output_filename.replace('.mp4', ''))}",
|
|
913
|
-
border_style="green"
|
|
914
|
-
))
|
|
915
|
-
|
|
916
|
-
# Handle missing segments
|
|
917
|
-
if missing_ts:
|
|
918
|
-
os.rename(self.output_filename, self.output_filename.replace(".mp4", "_failed.mp4"))
|
|
919
|
-
|
|
920
|
-
# Delete all temporary files except for the output file
|
|
921
|
-
os_manager.remove_files_except_one(self.path_manager.base_path, os.path.basename(self.output_filename.replace(".mp4", "_failed.mp4")))
|
|
922
|
-
|
|
923
|
-
# Remove the base folder if specified
|
|
924
|
-
if REMOVE_SEGMENTS_FOLDER:
|
|
925
|
-
os_manager.remove_folder(self.path_manager.base_path)
|
|
926
|
-
|
|
927
|
-
else:
|
|
928
|
-
logging.info("Video file converted already exists.")
|
|
929
|
-
|
|
930
|
-
def _valida_playlist(self):
|
|
931
|
-
"""
|
|
932
|
-
Validates the m3u8 playlist content, saves it to a temporary file, and collects playlist information.
|
|
933
|
-
"""
|
|
934
|
-
logging.info("class 'HLS_Downloader'; call _valida_playlist()")
|
|
935
|
-
|
|
936
|
-
# Retrieve the m3u8 playlist content
|
|
937
|
-
if self.is_playlist_url:
|
|
938
|
-
if self.request_m3u8_playlist != 404:
|
|
939
|
-
m3u8_playlist_text = self.request_m3u8_playlist
|
|
940
|
-
m3u8_url_fixer.set_playlist(self.m3u8_playlist)
|
|
941
|
-
|
|
942
|
-
else:
|
|
943
|
-
logging.info(f"class 'HLS_Downloader'; call _process_playlist(); return 404")
|
|
944
|
-
return 404
|
|
945
|
-
|
|
946
|
-
else:
|
|
947
|
-
m3u8_playlist_text = self.m3u8_playlist
|
|
948
|
-
|
|
949
|
-
# Check if the m3u8 content is valid
|
|
950
|
-
if m3u8_playlist_text is None:
|
|
951
|
-
console.log("[red]Playlist m3u8 to download is empty.")
|
|
952
|
-
sys.exit(0)
|
|
953
|
-
|
|
954
|
-
# Save the m3u8 playlist text to a temporary file
|
|
955
|
-
open(os.path.join(self.path_manager.base_temp, "playlist.m3u8"), "w+", encoding="utf-8").write(m3u8_playlist_text)
|
|
956
|
-
|
|
957
|
-
# Collect information about the playlist
|
|
958
|
-
if self.is_playlist_url:
|
|
959
|
-
self.content_extractor.start(self.instace_parserClass)
|
|
960
|
-
else:
|
|
961
|
-
self.content_extractor.start("https://fake.com", m3u8_playlist_text)
|
|
962
|
-
|
|
963
|
-
def _process_playlist(self):
|
|
964
|
-
"""
|
|
965
|
-
Processes the m3u8 playlist to download video, audio, and subtitles.
|
|
966
|
-
"""
|
|
967
|
-
self._valida_playlist()
|
|
968
|
-
|
|
969
|
-
# Add downloaded elements to the tracker
|
|
970
|
-
self.download_tracker.add_video(self.content_extractor.m3u8_index)
|
|
971
|
-
self.download_tracker.add_audio(self.content_extractor.list_available_audio)
|
|
972
|
-
self.download_tracker.add_subtitle(self.content_extractor.list_available_subtitles)
|
|
973
|
-
|
|
974
|
-
# Download each type of content
|
|
975
|
-
if DOWNLOAD_VIDEO and len(self.download_tracker.downloaded_video) > 0:
|
|
976
|
-
self.content_downloader.download_video(self.download_tracker.downloaded_video)
|
|
977
|
-
if DOWNLOAD_AUDIO and len(self.download_tracker.downloaded_audio) > 0:
|
|
978
|
-
self.content_downloader.download_audio(self.download_tracker.downloaded_audio)
|
|
979
|
-
if DOWNLOAD_SUBTITLE and len(self.download_tracker.downloaded_subtitle) > 0:
|
|
980
|
-
self.content_downloader.download_subtitle(self.download_tracker.downloaded_subtitle)
|
|
981
|
-
|
|
982
|
-
# Join downloaded content
|
|
983
|
-
self.content_joiner.setup(self.download_tracker.downloaded_video, self.download_tracker.downloaded_audio, self.download_tracker.downloaded_subtitle, self.content_extractor.codec)
|
|
984
|
-
|
|
985
|
-
# Clean up temporary files and directories
|
|
986
|
-
self._clean(self.content_joiner.converted_out_path)
|
|
987
|
-
|
|
988
|
-
def _process_index(self):
|
|
989
|
-
"""
|
|
990
|
-
Processes the m3u8 index to download only video.
|
|
991
|
-
"""
|
|
992
|
-
m3u8_url_fixer.set_playlist(self.m3u8_index)
|
|
993
|
-
|
|
994
|
-
# Download video
|
|
995
|
-
self.download_tracker.add_video(self.m3u8_index)
|
|
996
|
-
self.content_downloader.download_video(self.download_tracker.downloaded_video)
|
|
997
|
-
|
|
998
|
-
# Join video
|
|
999
|
-
self.content_joiner.setup(self.download_tracker.downloaded_video, [], [])
|
|
1000
|
-
|
|
1001
|
-
# Clean up temporary files and directories
|
|
1002
|
-
self._clean(self.content_joiner.converted_out_path)
|
|
1003
|
-
|
|
1004
|
-
def _reset(self):
|
|
1005
|
-
global list_MissingTs, m3u8_url_fixer
|
|
1006
|
-
|
|
1007
|
-
m3u8_url_fixer.reset_playlist()
|
|
1008
|
-
list_MissingTs = []
|
|
1
|
+
# 17.10.24
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
import time
|
|
6
|
+
import logging
|
|
7
|
+
import shutil
|
|
8
|
+
from typing import Any, Dict, List, Optional
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
# External libraries
|
|
12
|
+
import httpx
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# Internal utilities
|
|
16
|
+
from StreamingCommunity.Util._jsonConfig import config_manager
|
|
17
|
+
from StreamingCommunity.Util.headers import get_headers
|
|
18
|
+
from StreamingCommunity.Util.console import console, Panel
|
|
19
|
+
from StreamingCommunity.Util.os import (
|
|
20
|
+
compute_sha1_hash,
|
|
21
|
+
os_manager,
|
|
22
|
+
internet_manager
|
|
23
|
+
)
|
|
24
|
+
from StreamingCommunity.TelegramHelp.telegram_bot import get_bot_instance
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# Logic class
|
|
28
|
+
from ...FFmpeg import (
|
|
29
|
+
print_duration_table,
|
|
30
|
+
join_video,
|
|
31
|
+
join_audios,
|
|
32
|
+
join_subtitle
|
|
33
|
+
)
|
|
34
|
+
from ...M3U8 import M3U8_Parser, M3U8_UrlFix
|
|
35
|
+
from .segments import M3U8_Segments
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# Config
|
|
39
|
+
ENABLE_AUDIO = config_manager.get_bool('M3U8_DOWNLOAD', 'download_audio')
|
|
40
|
+
ENABLE_SUBTITLE = config_manager.get_bool('M3U8_DOWNLOAD', 'download_subtitle')
|
|
41
|
+
DOWNLOAD_SPECIFIC_AUDIO = config_manager.get_list('M3U8_DOWNLOAD', 'specific_list_audio')
|
|
42
|
+
DOWNLOAD_SPECIFIC_SUBTITLE = config_manager.get_list('M3U8_DOWNLOAD', 'specific_list_subtitles')
|
|
43
|
+
MERGE_AUDIO = config_manager.get_bool('M3U8_DOWNLOAD', 'merge_audio')
|
|
44
|
+
MERGE_SUBTITLE = config_manager.get_bool('M3U8_DOWNLOAD', 'merge_subs')
|
|
45
|
+
CLEANUP_TMP = config_manager.get_bool('M3U8_DOWNLOAD', 'cleanup_tmp_folder')
|
|
46
|
+
FILTER_CUSTOM_REOLUTION = config_manager.get_int('M3U8_PARSER', 'force_resolution')
|
|
47
|
+
GET_ONLY_LINK = config_manager.get_bool('M3U8_PARSER', 'get_only_link')
|
|
48
|
+
RETRY_LIMIT = config_manager.get_int('REQUESTS', 'max_retry')
|
|
49
|
+
MAX_TIMEOUT = config_manager.get_int("REQUESTS", "timeout")
|
|
50
|
+
|
|
51
|
+
TELEGRAM_BOT = config_manager.get_bool('DEFAULT', 'telegram_bot')
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class HLSClient:
|
|
56
|
+
"""Client for making HTTP requests to HLS endpoints with retry mechanism."""
|
|
57
|
+
def __init__(self):
|
|
58
|
+
self.headers = {'User-Agent': get_headers()}
|
|
59
|
+
|
|
60
|
+
def request(self, url: str, return_content: bool = False) -> Optional[httpx.Response]:
|
|
61
|
+
"""
|
|
62
|
+
Makes HTTP GET requests with retry logic.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
url: Target URL to request
|
|
66
|
+
return_content: If True, returns response content instead of text
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
Response content/text or None if all retries fail
|
|
70
|
+
"""
|
|
71
|
+
client = httpx.Client(headers=self.headers, timeout=MAX_TIMEOUT, follow_redirects=True)
|
|
72
|
+
for attempt in range(RETRY_LIMIT):
|
|
73
|
+
try:
|
|
74
|
+
response = client.get(url)
|
|
75
|
+
response.raise_for_status()
|
|
76
|
+
return response.content if return_content else response.text
|
|
77
|
+
|
|
78
|
+
except Exception as e:
|
|
79
|
+
logging.error(f"Attempt {attempt+1} failed: {str(e)}")
|
|
80
|
+
time.sleep(1.5 ** attempt)
|
|
81
|
+
return None
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class PathManager:
|
|
85
|
+
"""Manages file paths and directories for downloaded content."""
|
|
86
|
+
def __init__(self, m3u8_url: str, output_path: Optional[str]):
|
|
87
|
+
"""
|
|
88
|
+
Args:
|
|
89
|
+
m3u8_url: Source M3U8 playlist URL
|
|
90
|
+
output_path: Desired output path for the final video file
|
|
91
|
+
"""
|
|
92
|
+
self.m3u8_url = m3u8_url
|
|
93
|
+
self.output_path = self._sanitize_output_path(output_path)
|
|
94
|
+
base_name = os.path.basename(self.output_path).replace(".mp4", "")
|
|
95
|
+
self.temp_dir = os.path.join(os.path.dirname(self.output_path), f"{base_name}_tmp")
|
|
96
|
+
|
|
97
|
+
def _sanitize_output_path(self, path: Optional[str]) -> str:
|
|
98
|
+
"""
|
|
99
|
+
Ensures output path is valid and follows expected format.
|
|
100
|
+
Creates a hash-based filename if no path is provided.
|
|
101
|
+
"""
|
|
102
|
+
if not path:
|
|
103
|
+
root = config_manager.get('DEFAULT', 'root_path')
|
|
104
|
+
hash_name = compute_sha1_hash(self.m3u8_url) + ".mp4"
|
|
105
|
+
return os.path.join(root, "undefined", hash_name)
|
|
106
|
+
|
|
107
|
+
if not path.endswith(".mp4"):
|
|
108
|
+
path += ".mp4"
|
|
109
|
+
|
|
110
|
+
return os_manager.get_sanitize_path(path)
|
|
111
|
+
|
|
112
|
+
def setup_directories(self):
|
|
113
|
+
"""Creates necessary directories for temporary files (video, audio, subtitles)."""
|
|
114
|
+
os.makedirs(self.temp_dir, exist_ok=True)
|
|
115
|
+
for subdir in ['video', 'audio', 'subs']:
|
|
116
|
+
os.makedirs(os.path.join(self.temp_dir, subdir), exist_ok=True)
|
|
117
|
+
|
|
118
|
+
def move_final_file(self, final_file: str):
|
|
119
|
+
"""Moves the final merged file to the desired output location."""
|
|
120
|
+
if os.path.exists(self.output_path):
|
|
121
|
+
os.remove(self.output_path)
|
|
122
|
+
shutil.move(final_file, self.output_path)
|
|
123
|
+
|
|
124
|
+
def cleanup(self):
|
|
125
|
+
"""Removes temporary directories if configured to do so."""
|
|
126
|
+
if CLEANUP_TMP:
|
|
127
|
+
os_manager.remove_folder(self.temp_dir)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class M3U8Manager:
|
|
131
|
+
"""Handles M3U8 playlist parsing and stream selection."""
|
|
132
|
+
def __init__(self, m3u8_url: str, client: HLSClient):
|
|
133
|
+
self.m3u8_url = m3u8_url
|
|
134
|
+
self.client = client
|
|
135
|
+
self.parser = M3U8_Parser()
|
|
136
|
+
self.url_fixer = M3U8_UrlFix()
|
|
137
|
+
self.video_url = None
|
|
138
|
+
self.video_res = None
|
|
139
|
+
self.audio_streams = []
|
|
140
|
+
self.sub_streams = []
|
|
141
|
+
self.is_master = False
|
|
142
|
+
|
|
143
|
+
def parse(self):
|
|
144
|
+
"""
|
|
145
|
+
Fetches and parses the M3U8 playlist content.
|
|
146
|
+
Determines if it's a master playlist (index) or media playlist.
|
|
147
|
+
"""
|
|
148
|
+
content = self.client.request(self.m3u8_url)
|
|
149
|
+
if not content:
|
|
150
|
+
raise ValueError("Failed to fetch M3U8 content")
|
|
151
|
+
|
|
152
|
+
self.parser.parse_data(uri=self.m3u8_url, raw_content=content)
|
|
153
|
+
self.url_fixer.set_playlist(self.m3u8_url)
|
|
154
|
+
self.is_master = self.parser.is_master_playlist
|
|
155
|
+
|
|
156
|
+
def select_streams(self):
|
|
157
|
+
"""
|
|
158
|
+
Selects video, audio, and subtitle streams based on configuration.
|
|
159
|
+
If it's a master playlist, only selects video stream.
|
|
160
|
+
"""
|
|
161
|
+
if not self.is_master:
|
|
162
|
+
if FILTER_CUSTOM_REOLUTION != -1:
|
|
163
|
+
self.video_url, self.video_res = self.parser._video.get_custom_uri(y_resolution=FILTER_CUSTOM_REOLUTION)
|
|
164
|
+
else:
|
|
165
|
+
self.video_url, self.video_res = self.parser._video.get_best_uri()
|
|
166
|
+
|
|
167
|
+
self.audio_streams = []
|
|
168
|
+
self.sub_streams = []
|
|
169
|
+
|
|
170
|
+
else:
|
|
171
|
+
if FILTER_CUSTOM_REOLUTION != -1:
|
|
172
|
+
self.video_url, self.video_res = self.parser._video.get_custom_uri(y_resolution=FILTER_CUSTOM_REOLUTION)
|
|
173
|
+
else:
|
|
174
|
+
self.video_url, self.video_res = self.parser._video.get_best_uri()
|
|
175
|
+
|
|
176
|
+
self.audio_streams = []
|
|
177
|
+
if ENABLE_AUDIO:
|
|
178
|
+
self.audio_streams = [
|
|
179
|
+
s for s in (self.parser._audio.get_all_uris_and_names() or [])
|
|
180
|
+
if s.get('language') in DOWNLOAD_SPECIFIC_AUDIO
|
|
181
|
+
]
|
|
182
|
+
|
|
183
|
+
self.sub_streams = []
|
|
184
|
+
if ENABLE_SUBTITLE:
|
|
185
|
+
self.sub_streams = [
|
|
186
|
+
s for s in (self.parser._subtitle.get_all_uris_and_names() or [])
|
|
187
|
+
if s.get('language') in DOWNLOAD_SPECIFIC_SUBTITLE
|
|
188
|
+
]
|
|
189
|
+
|
|
190
|
+
def log_selection(self):
|
|
191
|
+
if FILTER_CUSTOM_REOLUTION == -1:
|
|
192
|
+
set_resolution = "Best"
|
|
193
|
+
else:
|
|
194
|
+
set_resolution = f"{FILTER_CUSTOM_REOLUTION}p"
|
|
195
|
+
|
|
196
|
+
tuple_available_resolution = self.parser._video.get_list_resolution()
|
|
197
|
+
list_available_resolution = [f"{r[0]}x{r[1]}" for r in tuple_available_resolution]
|
|
198
|
+
|
|
199
|
+
console.print(
|
|
200
|
+
f"[cyan bold]Video →[/cyan bold] [green]Available:[/green] [purple]{', '.join(list_available_resolution)}[/purple] | "
|
|
201
|
+
f"[red]Set:[/red] [purple]{set_resolution}[/purple] | "
|
|
202
|
+
f"[yellow]Downloadable:[/yellow] [purple]{self.video_res[0]}x{self.video_res[1]}[/purple]"
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
if self.parser.codec is not None:
|
|
206
|
+
available_codec_info = (
|
|
207
|
+
f"[green]v[/green]: [yellow]{self.parser.codec.video_codec_name}[/yellow] "
|
|
208
|
+
f"([green]b[/green]: [yellow]{self.parser.codec.video_bitrate // 1000}k[/yellow]), "
|
|
209
|
+
f"[green]a[/green]: [yellow]{self.parser.codec.audio_codec_name}[/yellow] "
|
|
210
|
+
f"([green]b[/green]: [yellow]{self.parser.codec.audio_bitrate // 1000}k[/yellow])"
|
|
211
|
+
)
|
|
212
|
+
set_codec_info = available_codec_info if config_manager.get_bool("M3U8_CONVERSION", "use_codec") else "[purple]copy[/purple]"
|
|
213
|
+
|
|
214
|
+
console.print(
|
|
215
|
+
f"[bold cyan]Codec →[/bold cyan] [green]Available:[/green] {available_codec_info} | "
|
|
216
|
+
f"[red]Set:[/red] {set_codec_info}"
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
available_subtitles = self.parser._subtitle.get_all_uris_and_names() or []
|
|
220
|
+
available_sub_languages = [sub.get('language') for sub in available_subtitles]
|
|
221
|
+
downloadable_sub_languages = list(set(available_sub_languages) & set(DOWNLOAD_SPECIFIC_SUBTITLE))
|
|
222
|
+
if available_sub_languages:
|
|
223
|
+
console.print(
|
|
224
|
+
f"[cyan bold]Subtitle →[/cyan bold] [green]Available:[/green] [purple]{', '.join(available_sub_languages)}[/purple] | "
|
|
225
|
+
f"[red]Set:[/red] [purple]{', '.join(DOWNLOAD_SPECIFIC_SUBTITLE)}[/purple] | "
|
|
226
|
+
f"[yellow]Downloadable:[/yellow] [purple]{', '.join(downloadable_sub_languages)}[/purple]"
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
available_audio = self.parser._audio.get_all_uris_and_names() or []
|
|
230
|
+
available_audio_languages = [audio.get('language') for audio in available_audio]
|
|
231
|
+
downloadable_audio_languages = list(set(available_audio_languages) & set(DOWNLOAD_SPECIFIC_AUDIO))
|
|
232
|
+
if available_audio_languages:
|
|
233
|
+
console.print(
|
|
234
|
+
f"[cyan bold]Audio →[/cyan bold] [green]Available:[/green] [purple]{', '.join(available_audio_languages)}[/purple] | "
|
|
235
|
+
f"[red]Set:[/red] [purple]{', '.join(DOWNLOAD_SPECIFIC_AUDIO)}[/purple] | "
|
|
236
|
+
f"[yellow]Downloadable:[/yellow] [purple]{', '.join(downloadable_audio_languages)}[/purple]"
|
|
237
|
+
)
|
|
238
|
+
print("")
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
class DownloadManager:
|
|
242
|
+
"""Manages downloading of video, audio, and subtitle streams."""
|
|
243
|
+
def __init__(self, temp_dir: str, client: HLSClient, url_fixer: M3U8_UrlFix):
|
|
244
|
+
"""
|
|
245
|
+
Args:
|
|
246
|
+
temp_dir: Directory for storing temporary files
|
|
247
|
+
client: HLSClient instance for making requests
|
|
248
|
+
url_fixer: URL fixer instance for generating complete URLs
|
|
249
|
+
"""
|
|
250
|
+
self.temp_dir = temp_dir
|
|
251
|
+
self.client = client
|
|
252
|
+
self.url_fixer = url_fixer
|
|
253
|
+
self.missing_segments = []
|
|
254
|
+
self.stopped = False
|
|
255
|
+
|
|
256
|
+
def download_video(self, video_url: str):
|
|
257
|
+
"""Downloads video segments from the M3U8 playlist."""
|
|
258
|
+
video_full_url = self.url_fixer.generate_full_url(video_url)
|
|
259
|
+
video_tmp_dir = os.path.join(self.temp_dir, 'video')
|
|
260
|
+
|
|
261
|
+
downloader = M3U8_Segments(url=video_full_url, tmp_folder=video_tmp_dir)
|
|
262
|
+
result = downloader.download_streams("Video", "video")
|
|
263
|
+
self.missing_segments.append(result)
|
|
264
|
+
|
|
265
|
+
if result.get('stopped', False):
|
|
266
|
+
self.stopped = True
|
|
267
|
+
return self.stopped
|
|
268
|
+
|
|
269
|
+
def download_audio(self, audio: Dict):
|
|
270
|
+
"""Downloads audio segments for a specific language track."""
|
|
271
|
+
if self.stopped:
|
|
272
|
+
return True
|
|
273
|
+
|
|
274
|
+
audio_full_url = self.url_fixer.generate_full_url(audio['uri'])
|
|
275
|
+
audio_tmp_dir = os.path.join(self.temp_dir, 'audio', audio['language'])
|
|
276
|
+
|
|
277
|
+
downloader = M3U8_Segments(url=audio_full_url, tmp_folder=audio_tmp_dir)
|
|
278
|
+
result = downloader.download_streams(f"Audio {audio['language']}", "audio")
|
|
279
|
+
self.missing_segments.append(result)
|
|
280
|
+
|
|
281
|
+
if result.get('stopped', False):
|
|
282
|
+
self.stopped = True
|
|
283
|
+
return self.stopped
|
|
284
|
+
|
|
285
|
+
def download_subtitle(self, sub: Dict):
|
|
286
|
+
"""Downloads and saves subtitle file for a specific language."""
|
|
287
|
+
if self.stopped:
|
|
288
|
+
return True
|
|
289
|
+
|
|
290
|
+
content = self.client.request(sub['uri'])
|
|
291
|
+
if content:
|
|
292
|
+
sub_path = os.path.join(self.temp_dir, 'subs', f"{sub['language']}.vtt")
|
|
293
|
+
with open(sub_path, 'w', encoding='utf-8') as f:
|
|
294
|
+
f.write(content)
|
|
295
|
+
|
|
296
|
+
return self.stopped
|
|
297
|
+
|
|
298
|
+
def download_all(self, video_url: str, audio_streams: List[Dict], sub_streams: List[Dict]):
|
|
299
|
+
"""
|
|
300
|
+
Downloads all selected streams (video, audio, subtitles).
|
|
301
|
+
"""
|
|
302
|
+
video_file = os.path.join(self.temp_dir, 'video', '0.ts')
|
|
303
|
+
if not os.path.exists(video_file):
|
|
304
|
+
if self.download_video(video_url):
|
|
305
|
+
return True
|
|
306
|
+
|
|
307
|
+
for audio in audio_streams:
|
|
308
|
+
if self.stopped:
|
|
309
|
+
break
|
|
310
|
+
|
|
311
|
+
audio_file = os.path.join(self.temp_dir, 'audio', audio['language'], '0.ts')
|
|
312
|
+
if not os.path.exists(audio_file):
|
|
313
|
+
if self.download_audio(audio):
|
|
314
|
+
return True
|
|
315
|
+
|
|
316
|
+
for sub in sub_streams:
|
|
317
|
+
if self.stopped:
|
|
318
|
+
break
|
|
319
|
+
|
|
320
|
+
sub_file = os.path.join(self.temp_dir, 'subs', f"{sub['language']}.vtt")
|
|
321
|
+
if not os.path.exists(sub_file):
|
|
322
|
+
if self.download_subtitle(sub):
|
|
323
|
+
return True
|
|
324
|
+
|
|
325
|
+
return self.stopped
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
class MergeManager:
|
|
329
|
+
"""Handles merging of video, audio, and subtitle streams."""
|
|
330
|
+
def __init__(self, temp_dir: str, parser: M3U8_Parser, audio_streams: List[Dict], sub_streams: List[Dict]):
|
|
331
|
+
"""
|
|
332
|
+
Args:
|
|
333
|
+
temp_dir: Directory containing temporary files
|
|
334
|
+
parser: M3U8 parser instance with codec information
|
|
335
|
+
audio_streams: List of audio streams to merge
|
|
336
|
+
sub_streams: List of subtitle streams to merge
|
|
337
|
+
"""
|
|
338
|
+
self.temp_dir = temp_dir
|
|
339
|
+
self.parser = parser
|
|
340
|
+
self.audio_streams = audio_streams
|
|
341
|
+
self.sub_streams = sub_streams
|
|
342
|
+
|
|
343
|
+
def merge(self) -> str:
|
|
344
|
+
"""
|
|
345
|
+
Merges downloaded streams into final video file.
|
|
346
|
+
Returns path to the final merged file.
|
|
347
|
+
|
|
348
|
+
Process:
|
|
349
|
+
1. If no audio/subs, just process video
|
|
350
|
+
2. If audio exists, merge with video
|
|
351
|
+
3. If subtitles exist, add them to the video
|
|
352
|
+
"""
|
|
353
|
+
video_file = os.path.join(self.temp_dir, 'video', '0.ts')
|
|
354
|
+
merged_file = video_file
|
|
355
|
+
|
|
356
|
+
if not self.audio_streams and not self.sub_streams:
|
|
357
|
+
merged_file = join_video(
|
|
358
|
+
video_path=video_file,
|
|
359
|
+
out_path=os.path.join(self.temp_dir, 'video.mp4'),
|
|
360
|
+
codec=self.parser.codec
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
else:
|
|
364
|
+
if MERGE_AUDIO and self.audio_streams:
|
|
365
|
+
audio_tracks = [{
|
|
366
|
+
'path': os.path.join(self.temp_dir, 'audio', a['language'], '0.ts'),
|
|
367
|
+
'name': a['language']
|
|
368
|
+
} for a in self.audio_streams]
|
|
369
|
+
|
|
370
|
+
merged_audio_path = os.path.join(self.temp_dir, 'merged_audio.mp4')
|
|
371
|
+
merged_file = join_audios(
|
|
372
|
+
video_path=video_file,
|
|
373
|
+
audio_tracks=audio_tracks,
|
|
374
|
+
out_path=merged_audio_path,
|
|
375
|
+
codec=self.parser.codec
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
if MERGE_SUBTITLE and self.sub_streams:
|
|
379
|
+
sub_tracks = [{
|
|
380
|
+
'path': os.path.join(self.temp_dir, 'subs', f"{s['language']}.vtt"),
|
|
381
|
+
'language': s['language']
|
|
382
|
+
} for s in self.sub_streams]
|
|
383
|
+
|
|
384
|
+
merged_subs_path = os.path.join(self.temp_dir, 'final.mp4')
|
|
385
|
+
merged_file = join_subtitle(
|
|
386
|
+
video_path=merged_file,
|
|
387
|
+
subtitles_list=sub_tracks,
|
|
388
|
+
out_path=merged_subs_path
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
return merged_file
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
class HLS_Downloader:
|
|
395
|
+
"""Main class for HLS video download and processing."""
|
|
396
|
+
def __init__(self, m3u8_url: str, output_path: Optional[str] = None):
|
|
397
|
+
self.m3u8_url = m3u8_url
|
|
398
|
+
self.path_manager = PathManager(m3u8_url, output_path)
|
|
399
|
+
self.client = HLSClient()
|
|
400
|
+
self.m3u8_manager = M3U8Manager(m3u8_url, self.client)
|
|
401
|
+
self.download_manager: Optional[DownloadManager] = None
|
|
402
|
+
self.merge_manager: Optional[MergeManager] = None
|
|
403
|
+
|
|
404
|
+
def start(self) -> Dict[str, Any]:
|
|
405
|
+
"""
|
|
406
|
+
Main execution flow with handling for both index and playlist M3U8s.
|
|
407
|
+
|
|
408
|
+
Returns:
|
|
409
|
+
Dict containing:
|
|
410
|
+
- path: Output file path
|
|
411
|
+
- url: Original M3U8 URL
|
|
412
|
+
- is_master: Whether the M3U8 was a master playlist
|
|
413
|
+
Or raises an exception if there's an error
|
|
414
|
+
"""
|
|
415
|
+
if TELEGRAM_BOT:
|
|
416
|
+
bot = get_bot_instance()
|
|
417
|
+
|
|
418
|
+
try:
|
|
419
|
+
if os.path.exists(self.path_manager.output_path):
|
|
420
|
+
console.print(f"[red]Output file {self.path_manager.output_path} already exists![/red]")
|
|
421
|
+
response = {
|
|
422
|
+
'path': self.path_manager.output_path,
|
|
423
|
+
'url': self.m3u8_url,
|
|
424
|
+
'is_master': False,
|
|
425
|
+
'error': 'File already exists',
|
|
426
|
+
'stopped': False
|
|
427
|
+
}
|
|
428
|
+
if TELEGRAM_BOT:
|
|
429
|
+
bot.send_message(response)
|
|
430
|
+
return response
|
|
431
|
+
|
|
432
|
+
self.path_manager.setup_directories()
|
|
433
|
+
|
|
434
|
+
# Parse M3U8 and determine if it's a master playlist
|
|
435
|
+
self.m3u8_manager.parse()
|
|
436
|
+
self.m3u8_manager.select_streams()
|
|
437
|
+
self.m3u8_manager.log_selection()
|
|
438
|
+
|
|
439
|
+
self.download_manager = DownloadManager(
|
|
440
|
+
temp_dir=self.path_manager.temp_dir,
|
|
441
|
+
client=self.client,
|
|
442
|
+
url_fixer=self.m3u8_manager.url_fixer
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
# Check if download was stopped
|
|
446
|
+
download_stopped = self.download_manager.download_all(
|
|
447
|
+
video_url=self.m3u8_manager.video_url,
|
|
448
|
+
audio_streams=self.m3u8_manager.audio_streams,
|
|
449
|
+
sub_streams=self.m3u8_manager.sub_streams
|
|
450
|
+
)
|
|
451
|
+
|
|
452
|
+
if download_stopped:
|
|
453
|
+
return {
|
|
454
|
+
'path': None,
|
|
455
|
+
'url': self.m3u8_url,
|
|
456
|
+
'is_master': self.m3u8_manager.is_master,
|
|
457
|
+
'error': 'Download stopped by user',
|
|
458
|
+
'stopped': True
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
self.merge_manager = MergeManager(
|
|
462
|
+
temp_dir=self.path_manager.temp_dir,
|
|
463
|
+
parser=self.m3u8_manager.parser,
|
|
464
|
+
audio_streams=self.m3u8_manager.audio_streams,
|
|
465
|
+
sub_streams=self.m3u8_manager.sub_streams
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
final_file = self.merge_manager.merge()
|
|
469
|
+
self.path_manager.move_final_file(final_file)
|
|
470
|
+
self.path_manager.cleanup()
|
|
471
|
+
|
|
472
|
+
self._print_summary()
|
|
473
|
+
|
|
474
|
+
return {
|
|
475
|
+
'path': self.path_manager.output_path,
|
|
476
|
+
'url': self.m3u8_url,
|
|
477
|
+
'is_master': self.m3u8_manager.is_master,
|
|
478
|
+
'stopped': False
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
except Exception as e:
|
|
482
|
+
error_msg = str(e)
|
|
483
|
+
console.print(f"[red]Download failed: {error_msg}[/red]")
|
|
484
|
+
logging.error("Download error", exc_info=True)
|
|
485
|
+
|
|
486
|
+
return {
|
|
487
|
+
'path': None,
|
|
488
|
+
'url': self.m3u8_url,
|
|
489
|
+
'is_master': getattr(self.m3u8_manager, 'is_master', None),
|
|
490
|
+
'error': error_msg,
|
|
491
|
+
'stopped': False
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
def _print_summary(self):
|
|
495
|
+
"""Prints download summary including file size, duration, and any missing segments."""
|
|
496
|
+
if TELEGRAM_BOT:
|
|
497
|
+
bot = get_bot_instance()
|
|
498
|
+
|
|
499
|
+
missing_ts = False
|
|
500
|
+
missing_info = ""
|
|
501
|
+
for item in self.download_manager.missing_segments:
|
|
502
|
+
if int(item['nFailed']) >= 1:
|
|
503
|
+
missing_ts = True
|
|
504
|
+
missing_info += f"[red]TS Failed: {item['nFailed']} {item['type']} tracks[/red]\n"
|
|
505
|
+
|
|
506
|
+
file_size = internet_manager.format_file_size(os.path.getsize(self.path_manager.output_path))
|
|
507
|
+
duration = print_duration_table(self.path_manager.output_path, description=False, return_string=True)
|
|
508
|
+
|
|
509
|
+
print()
|
|
510
|
+
panel_content = (
|
|
511
|
+
f"[cyan]File size: [bold red]{file_size}[/bold red]\n"
|
|
512
|
+
f"[cyan]Duration: [bold]{duration}[/bold]\n"
|
|
513
|
+
f"[cyan]Output: [bold]{os.path.abspath(self.path_manager.output_path)}[/bold]"
|
|
514
|
+
)
|
|
515
|
+
|
|
516
|
+
if TELEGRAM_BOT:
|
|
517
|
+
message = f"Download completato\nDimensione: {file_size}\nDurata: {duration}\nPercorso: {os.path.abspath(self.path_manager.output_path)}"
|
|
518
|
+
clean_message = re.sub(r'\[[a-zA-Z]+\]', '', message)
|
|
519
|
+
bot.send_message(clean_message, None)
|
|
520
|
+
|
|
521
|
+
if missing_ts:
|
|
522
|
+
panel_content += f"\n{missing_info}"
|
|
523
|
+
os.rename(self.path_manager.output_path, self.path_manager.output_path.replace(".mp4", "_failed.mp4"))
|
|
524
|
+
|
|
525
|
+
console.print(Panel(
|
|
526
|
+
panel_content,
|
|
527
|
+
title=f"{os.path.basename(self.path_manager.output_path.replace('.mp4', ''))}",
|
|
528
|
+
border_style="green"
|
|
529
|
+
))
|