Merge pull request #157 from arabcoders/dev

fix yt-dlp Impersonate target and webhook not firing
This commit is contained in:
Abdulmohsen 2025-01-02 19:16:26 +03:00 committed by GitHub
commit 5301e134ce
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 417 additions and 107 deletions

2
.vscode/launch.json vendored
View file

@ -30,6 +30,7 @@
"console": "internalConsole", "console": "internalConsole",
"justMyCode": true, "justMyCode": true,
"env": { "env": {
"PYDEVD_DISABLE_FILE_VALIDATION": "1",
"YTP_CONFIG_PATH": "${workspaceFolder}/var/config", "YTP_CONFIG_PATH": "${workspaceFolder}/var/config",
"YTP_DOWNLOAD_PATH": "${workspaceFolder}/var/downloads", "YTP_DOWNLOAD_PATH": "${workspaceFolder}/var/downloads",
"YTP_TEMP_PATH": "${workspaceFolder}/var/tmp", "YTP_TEMP_PATH": "${workspaceFolder}/var/tmp",
@ -39,6 +40,7 @@
"YTP_YTDL_DEBUG": "true", "YTP_YTDL_DEBUG": "true",
"YTP_MAX_WORKERS": "2", "YTP_MAX_WORKERS": "2",
"YTP_IGNORE_UI": "true", "YTP_IGNORE_UI": "true",
"YTP_PIP_IGNORE_UPDATES": "true",
} }
}, },
{ {

View file

@ -19,6 +19,9 @@ curl_cffi = "*"
pysubs2 = "*" pysubs2 = "*"
regex = "*" regex = "*"
yt-dlp = "*" yt-dlp = "*"
mutagen = "*"
brotli = "*"
brotlicffi = "*"
[dev-packages] [dev-packages]

176
Pipfile.lock generated
View file

@ -1,7 +1,7 @@
{ {
"_meta": { "_meta": {
"hash": { "hash": {
"sha256": "d132a1657486171e8fddcc57ff4eb235886b74892116c54795c1d8122700e2f9" "sha256": "cd9a56440fe36b901993ced28ee6ef8d301a5125344cda86519fb7c54ede1449"
}, },
"pipfile-spec": 6, "pipfile-spec": 6,
"requires": { "requires": {
@ -156,6 +156,171 @@
"markers": "python_version >= '3.8'", "markers": "python_version >= '3.8'",
"version": "==0.23.1" "version": "==0.23.1"
}, },
"brotli": {
"hashes": [
"sha256:03d20af184290887bdea3f0f78c4f737d126c74dc2f3ccadf07e54ceca3bf208",
"sha256:0541e747cce78e24ea12d69176f6a7ddb690e62c425e01d31cc065e69ce55b48",
"sha256:069a121ac97412d1fe506da790b3e69f52254b9df4eb665cd42460c837193354",
"sha256:0737ddb3068957cf1b054899b0883830bb1fec522ec76b1098f9b6e0f02d9419",
"sha256:0b63b949ff929fbc2d6d3ce0e924c9b93c9785d877a21a1b678877ffbbc4423a",
"sha256:0c6244521dda65ea562d5a69b9a26120769b7a9fb3db2fe9545935ed6735b128",
"sha256:11d00ed0a83fa22d29bc6b64ef636c4552ebafcef57154b4ddd132f5638fbd1c",
"sha256:141bd4d93984070e097521ed07e2575b46f817d08f9fa42b16b9b5f27b5ac088",
"sha256:19c116e796420b0cee3da1ccec3b764ed2952ccfcc298b55a10e5610ad7885f9",
"sha256:1ab4fbee0b2d9098c74f3057b2bc055a8bd92ccf02f65944a241b4349229185a",
"sha256:1ae56aca0402a0f9a3431cddda62ad71666ca9d4dc3a10a142b9dce2e3c0cda3",
"sha256:1b2c248cd517c222d89e74669a4adfa5577e06ab68771a529060cf5a156e9757",
"sha256:1e9a65b5736232e7a7f91ff3d02277f11d339bf34099a56cdab6a8b3410a02b2",
"sha256:224e57f6eac61cc449f498cc5f0e1725ba2071a3d4f48d5d9dffba42db196438",
"sha256:22fc2a8549ffe699bfba2256ab2ed0421a7b8fadff114a3d201794e45a9ff578",
"sha256:23032ae55523cc7bccb4f6a0bf368cd25ad9bcdcc1990b64a647e7bbcce9cb5b",
"sha256:2333e30a5e00fe0fe55903c8832e08ee9c3b1382aacf4db26664a16528d51b4b",
"sha256:2954c1c23f81c2eaf0b0717d9380bd348578a94161a65b3a2afc62c86467dd68",
"sha256:2a24c50840d89ded6c9a8fdc7b6ed3692ed4e86f1c4a4a938e1e92def92933e0",
"sha256:2de9d02f5bda03d27ede52e8cfe7b865b066fa49258cbab568720aa5be80a47d",
"sha256:2feb1d960f760a575dbc5ab3b1c00504b24caaf6986e2dc2b01c09c87866a943",
"sha256:30924eb4c57903d5a7526b08ef4a584acc22ab1ffa085faceb521521d2de32dd",
"sha256:316cc9b17edf613ac76b1f1f305d2a748f1b976b033b049a6ecdfd5612c70409",
"sha256:32d95b80260d79926f5fab3c41701dbb818fde1c9da590e77e571eefd14abe28",
"sha256:38025d9f30cf4634f8309c6874ef871b841eb3c347e90b0851f63d1ded5212da",
"sha256:39da8adedf6942d76dc3e46653e52df937a3c4d6d18fdc94a7c29d263b1f5b50",
"sha256:3c0ef38c7a7014ffac184db9e04debe495d317cc9c6fb10071f7fefd93100a4f",
"sha256:3d7954194c36e304e1523f55d7042c59dc53ec20dd4e9ea9d151f1b62b4415c0",
"sha256:3ee8a80d67a4334482d9712b8e83ca6b1d9bc7e351931252ebef5d8f7335a547",
"sha256:4093c631e96fdd49e0377a9c167bfd75b6d0bad2ace734c6eb20b348bc3ea180",
"sha256:43395e90523f9c23a3d5bdf004733246fba087f2948f87ab28015f12359ca6a0",
"sha256:43ce1b9935bfa1ede40028054d7f48b5469cd02733a365eec8a329ffd342915d",
"sha256:4410f84b33374409552ac9b6903507cdb31cd30d2501fc5ca13d18f73548444a",
"sha256:494994f807ba0b92092a163a0a283961369a65f6cbe01e8891132b7a320e61eb",
"sha256:4d4a848d1837973bf0f4b5e54e3bec977d99be36a7895c61abb659301b02c112",
"sha256:4ed11165dd45ce798d99a136808a794a748d5dc38511303239d4e2363c0695dc",
"sha256:4f3607b129417e111e30637af1b56f24f7a49e64763253bbc275c75fa887d4b2",
"sha256:510b5b1bfbe20e1a7b3baf5fed9e9451873559a976c1a78eebaa3b86c57b4265",
"sha256:524f35912131cc2cabb00edfd8d573b07f2d9f21fa824bd3fb19725a9cf06327",
"sha256:587ca6d3cef6e4e868102672d3bd9dc9698c309ba56d41c2b9c85bbb903cdb95",
"sha256:58d4b711689366d4a03ac7957ab8c28890415e267f9b6589969e74b6e42225ec",
"sha256:5b3cc074004d968722f51e550b41a27be656ec48f8afaeeb45ebf65b561481dd",
"sha256:5dab0844f2cf82be357a0eb11a9087f70c5430b2c241493fc122bb6f2bb0917c",
"sha256:5e55da2c8724191e5b557f8e18943b1b4839b8efc3ef60d65985bcf6f587dd38",
"sha256:5eeb539606f18a0b232d4ba45adccde4125592f3f636a6182b4a8a436548b914",
"sha256:5f4d5ea15c9382135076d2fb28dde923352fe02951e66935a9efaac8f10e81b0",
"sha256:5fb2ce4b8045c78ebbc7b8f3c15062e435d47e7393cc57c25115cfd49883747a",
"sha256:6172447e1b368dcbc458925e5ddaf9113477b0ed542df258d84fa28fc45ceea7",
"sha256:6967ced6730aed543b8673008b5a391c3b1076d834ca438bbd70635c73775368",
"sha256:6974f52a02321b36847cd19d1b8e381bf39939c21efd6ee2fc13a28b0d99348c",
"sha256:6c3020404e0b5eefd7c9485ccf8393cfb75ec38ce75586e046573c9dc29967a0",
"sha256:6c6e0c425f22c1c719c42670d561ad682f7bfeeef918edea971a79ac5252437f",
"sha256:70051525001750221daa10907c77830bc889cb6d865cc0b813d9db7fefc21451",
"sha256:7905193081db9bfa73b1219140b3d315831cbff0d8941f22da695832f0dd188f",
"sha256:7bc37c4d6b87fb1017ea28c9508b36bbcb0c3d18b4260fcdf08b200c74a6aee8",
"sha256:7c4855522edb2e6ae7fdb58e07c3ba9111e7621a8956f481c68d5d979c93032e",
"sha256:7e4c4629ddad63006efa0ef968c8e4751c5868ff0b1c5c40f76524e894c50248",
"sha256:7eedaa5d036d9336c95915035fb57422054014ebdeb6f3b42eac809928e40d0c",
"sha256:7f4bf76817c14aa98cc6697ac02f3972cb8c3da93e9ef16b9c66573a68014f91",
"sha256:81de08ac11bcb85841e440c13611c00b67d3bf82698314928d0b676362546724",
"sha256:832436e59afb93e1836081a20f324cb185836c617659b07b129141a8426973c7",
"sha256:861bf317735688269936f755fa136a99d1ed526883859f86e41a5d43c61d8966",
"sha256:87a3044c3a35055527ac75e419dfa9f4f3667a1e887ee80360589eb8c90aabb9",
"sha256:890b5a14ce214389b2cc36ce82f3093f96f4cc730c1cffdbefff77a7c71f2a97",
"sha256:89f4988c7203739d48c6f806f1e87a1d96e0806d44f0fba61dba81392c9e474d",
"sha256:8bf32b98b75c13ec7cf774164172683d6e7891088f6316e54425fde1efc276d5",
"sha256:8dadd1314583ec0bf2d1379f7008ad627cd6336625d6679cf2f8e67081b83acf",
"sha256:901032ff242d479a0efa956d853d16875d42157f98951c0230f69e69f9c09bac",
"sha256:9011560a466d2eb3f5a6e4929cf4a09be405c64154e12df0dd72713f6500e32b",
"sha256:906bc3a79de8c4ae5b86d3d75a8b77e44404b0f4261714306e3ad248d8ab0951",
"sha256:919e32f147ae93a09fe064d77d5ebf4e35502a8df75c29fb05788528e330fe74",
"sha256:91d7cc2a76b5567591d12c01f019dd7afce6ba8cba6571187e21e2fc418ae648",
"sha256:929811df5462e182b13920da56c6e0284af407d1de637d8e536c5cd00a7daf60",
"sha256:949f3b7c29912693cee0afcf09acd6ebc04c57af949d9bf77d6101ebb61e388c",
"sha256:a090ca607cbb6a34b0391776f0cb48062081f5f60ddcce5d11838e67a01928d1",
"sha256:a1fd8a29719ccce974d523580987b7f8229aeace506952fa9ce1d53a033873c8",
"sha256:a37b8f0391212d29b3a91a799c8e4a2855e0576911cdfb2515487e30e322253d",
"sha256:a3daabb76a78f829cafc365531c972016e4aa8d5b4bf60660ad8ecee19df7ccc",
"sha256:a469274ad18dc0e4d316eefa616d1d0c2ff9da369af19fa6f3daa4f09671fd61",
"sha256:a599669fd7c47233438a56936988a2478685e74854088ef5293802123b5b2460",
"sha256:a743e5a28af5f70f9c080380a5f908d4d21d40e8f0e0c8901604d15cfa9ba751",
"sha256:a77def80806c421b4b0af06f45d65a136e7ac0bdca3c09d9e2ea4e515367c7e9",
"sha256:a7e53012d2853a07a4a79c00643832161a910674a893d296c9f1259859a289d2",
"sha256:a93dde851926f4f2678e704fadeb39e16c35d8baebd5252c9fd94ce8ce68c4a0",
"sha256:aac0411d20e345dc0920bdec5548e438e999ff68d77564d5e9463a7ca9d3e7b1",
"sha256:ae15b066e5ad21366600ebec29a7ccbc86812ed267e4b28e860b8ca16a2bc474",
"sha256:aea440a510e14e818e67bfc4027880e2fb500c2ccb20ab21c7a7c8b5b4703d75",
"sha256:af6fa6817889314555aede9a919612b23739395ce767fe7fcbea9a80bf140fe5",
"sha256:b760c65308ff1e462f65d69c12e4ae085cff3b332d894637f6273a12a482d09f",
"sha256:be36e3d172dc816333f33520154d708a2657ea63762ec16b62ece02ab5e4daf2",
"sha256:c247dd99d39e0338a604f8c2b3bc7061d5c2e9e2ac7ba9cc1be5a69cb6cd832f",
"sha256:c5529b34c1c9d937168297f2c1fde7ebe9ebdd5e121297ff9c043bdb2ae3d6fb",
"sha256:c8146669223164fc87a7e3de9f81e9423c67a79d6b3447994dfb9c95da16e2d6",
"sha256:c8fd5270e906eef71d4a8d19b7c6a43760c6abcfcc10c9101d14eb2357418de9",
"sha256:ca63e1890ede90b2e4454f9a65135a4d387a4585ff8282bb72964fab893f2111",
"sha256:caf9ee9a5775f3111642d33b86237b05808dafcd6268faa492250e9b78046eb2",
"sha256:cb1dac1770878ade83f2ccdf7d25e494f05c9165f5246b46a621cc849341dc01",
"sha256:cdad5b9014d83ca68c25d2e9444e28e967ef16e80f6b436918c700c117a85467",
"sha256:cdbc1fc1bc0bff1cef838eafe581b55bfbffaed4ed0318b724d0b71d4d377619",
"sha256:ceb64bbc6eac5a140ca649003756940f8d6a7c444a68af170b3187623b43bebf",
"sha256:d0c5516f0aed654134a2fc936325cc2e642f8a0e096d075209672eb321cff408",
"sha256:d143fd47fad1db3d7c27a1b1d66162e855b5d50a89666af46e1679c496e8e579",
"sha256:d192f0f30804e55db0d0e0a35d83a9fead0e9a359a9ed0285dbacea60cc10a84",
"sha256:d2b35ca2c7f81d173d2fadc2f4f31e88cc5f7a39ae5b6db5513cf3383b0e0ec7",
"sha256:d342778ef319e1026af243ed0a07c97acf3bad33b9f29e7ae6a1f68fd083e90c",
"sha256:d487f5432bf35b60ed625d7e1b448e2dc855422e87469e3f450aa5552b0eb284",
"sha256:d7702622a8b40c49bffb46e1e3ba2e81268d5c04a34f460978c6b5517a34dd52",
"sha256:db85ecf4e609a48f4b29055f1e144231b90edc90af7481aa731ba2d059226b1b",
"sha256:de6551e370ef19f8de1807d0a9aa2cdfdce2e85ce88b122fe9f6b2b076837e59",
"sha256:e1140c64812cb9b06c922e77f1c26a75ec5e3f0fb2bf92cc8c58720dec276752",
"sha256:e4fe605b917c70283db7dfe5ada75e04561479075761a0b3866c081d035b01c1",
"sha256:e6a904cb26bfefc2f0a6f240bdf5233be78cd2488900a2f846f3c3ac8489ab80",
"sha256:e79e6520141d792237c70bcd7a3b122d00f2613769ae0cb61c52e89fd3443839",
"sha256:e84799f09591700a4154154cab9787452925578841a94321d5ee8fb9a9a328f0",
"sha256:e93dfc1a1165e385cc8239fab7c036fb2cd8093728cbd85097b284d7b99249a2",
"sha256:efa8b278894b14d6da122a72fefcebc28445f2d3f880ac59d46c90f4c13be9a3",
"sha256:f0d8a7a6b5983c2496e364b969f0e526647a06b075d034f3297dc66f3b360c64",
"sha256:f0db75f47be8b8abc8d9e31bc7aad0547ca26f24a54e6fd10231d623f183d089",
"sha256:f296c40e23065d0d6650c4aefe7470d2a25fffda489bcc3eb66083f3ac9f6643",
"sha256:f31859074d57b4639318523d6ffdca586ace54271a73ad23ad021acd807eb14b",
"sha256:f66b5337fa213f1da0d9000bc8dc0cb5b896b726eefd9c6046f699b169c41b9e",
"sha256:f733d788519c7e3e71f0855c96618720f5d3d60c3cb829d8bbb722dddce37985",
"sha256:fce1473f3ccc4187f75b4690cfc922628aed4d3dd013d047f95a9b3919a86596",
"sha256:fd5f17ff8f14003595ab414e45fce13d073e0762394f957182e69035c9f3d7c2",
"sha256:fdc3ff3bfccdc6b9cc7c342c03aa2400683f0cb891d46e94b64a197910dc4064"
],
"index": "pypi",
"version": "==1.1.0"
},
"brotlicffi": {
"hashes": [
"sha256:19ffc919fa4fc6ace69286e0a23b3789b4219058313cf9b45625016bf7ff996b",
"sha256:1a807d760763e398bbf2c6394ae9da5815901aa93ee0a37bca5efe78d4ee3171",
"sha256:1b12b50e07c3911e1efa3a8971543e7648100713d4e0971b13631cce22c587eb",
"sha256:246f1d1a90279bb6069de3de8d75a8856e073b8ff0b09dcca18ccc14cec85979",
"sha256:2a7ae37e5d79c5bdfb5b4b99f2715a6035e6c5bf538c3746abc8e26694f92f33",
"sha256:2e4aeb0bd2540cb91b069dbdd54d458da8c4334ceaf2d25df2f4af576d6766ca",
"sha256:2f3711be9290f0453de8eed5275d93d286abe26b08ab4a35d7452caa1fef532f",
"sha256:37c26ecb14386a44b118ce36e546ce307f4810bc9598a6e6cb4f7fca725ae7e6",
"sha256:391151ec86bb1c683835980f4816272a87eaddc46bb91cbf44f62228b84d8cca",
"sha256:3de0cf28a53a3238b252aca9fed1593e9d36c1d116748013339f0949bfc84112",
"sha256:4b7b0033b0d37bb33009fb2fef73310e432e76f688af76c156b3594389d81391",
"sha256:54a07bb2374a1eba8ebb52b6fafffa2afd3c4df85ddd38fcc0511f2bb387c2a8",
"sha256:6be5ec0e88a4925c91f3dea2bb0013b3a2accda6f77238f76a34a1ea532a1cb0",
"sha256:7901a7dc4b88f1c1475de59ae9be59799db1007b7d059817948d8e4f12e24e35",
"sha256:84763dbdef5dd5c24b75597a77e1b30c66604725707565188ba54bab4f114820",
"sha256:8557a8559509b61e65083f8782329188a250102372576093c88930c875a69838",
"sha256:994a4f0681bb6c6c3b0925530a1926b7a189d878e6e5e38fae8efa47c5d9c613",
"sha256:9b6068e0f3769992d6b622a1cd2e7835eae3cf8d9da123d7f51ca9c1e9c333e5",
"sha256:9b7ae6bd1a3f0df532b6d67ff674099a96d22bc0948955cb338488c31bfb8851",
"sha256:9feb210d932ffe7798ee62e6145d3a757eb6233aa9a4e7db78dd3690d7755814",
"sha256:add0de5b9ad9e9aa293c3aa4e9deb2b61e99ad6c1634e01d01d98c03e6a354cc",
"sha256:b77827a689905143f87915310b93b273ab17888fd43ef350d4832c4a71083c13",
"sha256:ca72968ae4eaf6470498d5c2887073f7efe3b1e7d7ec8be11a06a79cc810e990",
"sha256:cc4bc5d82bc56ebd8b514fb8350cfac4627d6b0743382e46d033976a5f80fab6",
"sha256:ce01c7316aebc7fce59da734286148b1d1b9455f89cf2c8a4dfce7d41db55c2d",
"sha256:d9eb71bb1085d996244439154387266fd23d6ad37161f6f52f1cd41dd95a3808",
"sha256:fa8ca0623b26c94fccc3a1fdd895be1743b838f3917300506d04aa3346fd2a14"
],
"index": "pypi",
"markers": "python_version >= '3.7'",
"version": "==1.1.0.0"
},
"caribou": { "caribou": {
"hashes": [ "hashes": [
"sha256:13a66fc9ef9b1c9e9ef220876d59a44ee5eff9e579655252271b7514fc0ad787", "sha256:13a66fc9ef9b1c9e9ef220876d59a44ee5eff9e579655252271b7514fc0ad787",
@ -549,6 +714,15 @@
"markers": "python_version >= '3.8'", "markers": "python_version >= '3.8'",
"version": "==6.1.0" "version": "==6.1.0"
}, },
"mutagen": {
"hashes": [
"sha256:719fadef0a978c31b4cf3c956261b3c58b6948b32023078a2117b1de09f0fc99",
"sha256:edd96f50c5907a9539d8e5bba7245f62c9f520aef333d13392a79a4f70aca719"
],
"index": "pypi",
"markers": "python_version >= '3.7'",
"version": "==1.47.0"
},
"propcache": { "propcache": {
"hashes": [ "hashes": [
"sha256:03ff9d3f665769b2a85e6157ac8b439644f2d7fd17615a82fa55739bc97863f4", "sha256:03ff9d3f665769b2a85e6157ac8b439644f2d7fd17615a82fa55739bc97863f4",

View file

@ -92,6 +92,8 @@ Certain values can be set via environment variables, using the `-e` parameter on
* __YTP_DB_FILE__: The path to the SQLite database file. Defaults to `{config_path}/ytptube.db`. * __YTP_DB_FILE__: The path to the SQLite database file. Defaults to `{config_path}/ytptube.db`.
* __YTP_MANUAL_ARCHIVE__: The path to the manual archive file. Defaults to `{config_path}/manual_archive.log`. * __YTP_MANUAL_ARCHIVE__: The path to the manual archive file. Defaults to `{config_path}/manual_archive.log`.
* __YTP_UI_UPDATE_TITLE__: Whether to update the title of the page with the current stats. Defaults to `true`. * __YTP_UI_UPDATE_TITLE__: Whether to update the title of the page with the current stats. Defaults to `true`.
* __YTP_PIP_PACKAGES__: a space separated list of pip packages to install. Defaults to `""`, you can also use `{config_path}/pip.txt` to install the packages.
* __YTP_PIP_IGNORE_UPDATES__: Do not update the custom pip packages. Defaults to `false`.
## Running behind a reverse proxy ## Running behind a reverse proxy

View file

@ -2,7 +2,7 @@ import math
import os import os
from urllib.parse import quote from urllib.parse import quote
from .Utils import calcDownloadPath, StreamingError from .Utils import calcDownloadPath, StreamingError
from .ffprobe import FFProbe from .ffprobe import ffprobe
class M3u8: class M3u8:
@ -30,15 +30,14 @@ class M3u8:
raise StreamingError(f"File '{realFile}' does not exist.") raise StreamingError(f"File '{realFile}' does not exist.")
try: try:
ffprobe = FFProbe(realFile) ff = await ffprobe(realFile)
await ffprobe.run()
except UnicodeDecodeError: except UnicodeDecodeError:
pass pass
if "duration" not in ffprobe.metadata: if "duration" not in ff.metadata:
raise StreamingError(f"Unable to get '{realFile}' play duration.") raise StreamingError(f"Unable to get '{realFile}' play duration.")
duration: float = float(ffprobe.metadata.get("duration")) duration: float = float(ff.metadata.get("duration"))
m3u8 = [] m3u8 = []
@ -53,7 +52,7 @@ class M3u8:
segmentParams: dict = {} segmentParams: dict = {}
for stream in ffprobe.streams: for stream in ff.streams():
if stream.is_video(): if stream.is_video():
if stream.codec_name not in self.ok_vcodecs: if stream.codec_name not in self.ok_vcodecs:
segmentParams["vc"] = 1 segmentParams["vc"] = 1

View file

@ -0,0 +1,61 @@
import logging
import os
import subprocess
import sys
import importlib
from library.config import Config
LOG = logging.getLogger("package_installer")
class PackageInstaller:
"""
This class is responsible for installing and upgrading pip packages.
"""
def __init__(self, config: Config):
self.config: Config = config
def from_env(self):
return self.config.pip_packages.split() if self.config.pip_packages else []
def from_file(self, file_path):
if not os.path.exists(file_path):
return []
if os.access(file_path, os.R_OK):
with open(file_path, "r") as f:
return [pkg.strip() for pkg in f.readlines() if pkg.strip()]
else:
LOG.error(f"Could not read pip packages from '{file_path}'.")
return []
def action(self, pkg: str):
try:
importlib.import_module(pkg)
if self.config.pip_ignore_updates:
LOG.info(f"'{pkg}' is already installed. Skipping upgrades. as requested.")
return
LOG.info(f"'{pkg}' is already installed. Checking for upgrades...")
subprocess.run([sys.executable, "-m", "pip", "install", "--upgrade", pkg], check=True)
except ImportError:
LOG.info(f"'{pkg}' is not installed. Installing...")
subprocess.run([sys.executable, "-m", "pip", "install", pkg], check=True)
def check(self):
"""
Checks for user supplied pip packages and installs them if they are not already installed.
"""
pip_file = os.path.join(self.config.config_path, "pip.txt")
packages = list(set(self.from_env() + self.from_file(pip_file)))
if not packages:
return
LOG.info(f"Checking for user pip packages: {', '.join(packages)}")
for package in packages:
try:
self.action(package)
except Exception as e:
LOG.error(f"Failed to install or upgrade package '{package}'. Error message: {str(e)}")
LOG.exception(e)

View file

@ -2,10 +2,8 @@ import glob
import re import re
from pathlib import Path from pathlib import Path
from urllib.parse import quote from urllib.parse import quote
from aiohttp.web import Response from aiohttp.web import Response
from .ffprobe import ffprobe
from .ffprobe import FFProbe
from .Subtitle import Subtitle from .Subtitle import Subtitle
from .Utils import calcDownloadPath, checkId, StreamingError from .Utils import calcDownloadPath, checkId, StreamingError
@ -32,15 +30,14 @@ class Playlist:
) )
try: try:
ffprobe = FFProbe(rFile) ff = await ffprobe(rFile)
await ffprobe.run()
except UnicodeDecodeError: except UnicodeDecodeError:
pass pass
if "duration" not in ffprobe.metadata: if "duration" not in ff.metadata:
raise StreamingError(f"Unable to get '{rFile}' duration.") raise StreamingError(f"Unable to get '{rFile}' duration.")
duration: float = float(ffprobe.metadata.get("duration")) duration: float = float(ff.metadata.get("duration"))
playlist = [] playlist = []
playlist.append("#EXTM3U") playlist.append("#EXTM3U")

View file

@ -4,6 +4,7 @@ import logging
import os import os
import tempfile import tempfile
from .ffprobe import ffprobe
from .config import Config from .config import Config
from .Utils import calcDownloadPath, StreamingError from .Utils import calcDownloadPath, StreamingError
@ -21,6 +22,7 @@ class Segments:
self.acodec = config.streamer_acodec self.acodec = config.streamer_acodec
# sadly due to unforeseen circumstances, we have to convert the video for now. # sadly due to unforeseen circumstances, we have to convert the video for now.
self.vconvert = True self.vconvert = True
self.aconvert = True
async def stream(self, path: str, file: str) -> bytes: async def stream(self, path: str, file: str) -> bytes:
realFile: str = calcDownloadPath(basePath=path, folder=file, createPath=False) realFile: str = calcDownloadPath(basePath=path, folder=file, createPath=False)
@ -28,6 +30,11 @@ class Segments:
if not os.path.exists(realFile): if not os.path.exists(realFile):
raise StreamingError(f"File {realFile} does not exist.") raise StreamingError(f"File {realFile} does not exist.")
try:
ff = await ffprobe(realFile)
except UnicodeDecodeError:
pass
tmpDir: str = tempfile.gettempdir() tmpDir: str = tempfile.gettempdir()
tmpFile = os.path.join(tmpDir, f'ytptube_stream.{hashlib.md5(realFile.encode("utf-8")).hexdigest()}') tmpFile = os.path.join(tmpDir, f'ytptube_stream.{hashlib.md5(realFile.encode("utf-8")).hexdigest()}')
@ -57,24 +64,27 @@ class Segments:
fargs.append("-map_metadata") fargs.append("-map_metadata")
fargs.append("-1") fargs.append("-1")
fargs.append("-pix_fmt") # video section.
fargs.append("yuv420p") if ff.has_video():
fargs.append("-g") fargs.append("-pix_fmt")
fargs.append("52") fargs.append("yuv420p")
fargs.append("-g")
fargs.append("52")
fargs.append("-map") fargs.append("-map")
fargs.append("0:v:0") fargs.append("0:v:0")
fargs.append("-strict") fargs.append("-strict")
fargs.append("-2") fargs.append("-2")
fargs.append("-codec:v") fargs.append("-codec:v")
fargs.append(self.vcodec if self.vconvert else "copy") fargs.append(self.vcodec if self.vconvert else "copy")
# audio section. # audio section.
fargs.append("-map") if ff.has_audio():
fargs.append("0:a:0") fargs.append("-map")
fargs.append("-codec:a") fargs.append("0:a:0")
fargs.append(self.acodec if self.aconvert else "copy") fargs.append("-codec:a")
fargs.append(self.acodec if self.aconvert else "copy")
fargs.append("-sn") fargs.append("-sn")

View file

@ -12,6 +12,7 @@ from functools import lru_cache
from typing import Any from typing import Any
import yt_dlp import yt_dlp
from yt_dlp.networking.impersonate import ImpersonateTarget
LOG = logging.getLogger("Utils") LOG = logging.getLogger("Utils")
@ -157,7 +158,16 @@ def mergeDict(source: dict, destination: dict) -> dict:
def mergeConfig(config: dict, new_config: dict) -> dict: def mergeConfig(config: dict, new_config: dict) -> dict:
"""Merge user provided config into default config""" """
Merge user provided config into default config
Args:
config (dict): Default config
new_config (dict): User provided config
Returns:
dict: Merged config
"""
ignored_keys: tuple = ( ignored_keys: tuple = (
"cookiefile", "cookiefile",
@ -171,7 +181,12 @@ def mergeConfig(config: dict, new_config: dict) -> dict:
if key in new_config: if key in new_config:
del new_config[key] del new_config[key]
return mergeDict(new_config, config) conf = mergeDict(new_config, config)
if "impersonate" in conf:
conf["impersonate"] = ImpersonateTarget.from_str(conf["impersonate"])
return conf
def isDownloaded(archive_file: str, url: str) -> tuple[bool, dict[str | None, str | None, str | None]]: def isDownloaded(archive_file: str, url: str) -> tuple[bool, dict[str | None, str | None, str | None]]:

View file

@ -29,20 +29,24 @@ class Webhooks:
LOG.info(f"Loading webhooks from '{file}'.") LOG.info(f"Loading webhooks from '{file}'.")
from .Utils import load_file from .Utils import load_file
(target, status, error) = load_file(file, list) (targets, status, error) = load_file(file, list)
if not status: if not status:
raise Exception(f"{error}") raise Exception(f"{error}")
self.targets = target for target in targets:
LOG.info(f"Will send '{target.get('on',['all'])}' events to '{target.get('name')}'.")
self.targets = targets
except Exception as e: except Exception as e:
LOG.error(f"Error loading webhooks from '{file}'. '{e}'") LOG.error(f"Error loading webhooks from '{file}'. '{e}'")
pass pass
async def send(self, event: str, item: ItemDTO) -> list[dict]: async def send(self, event: str, item: ItemDTO | dict) -> list[dict]:
if len(self.targets) == 0: if len(self.targets) == 0:
return [] return []
if not isinstance(item, ItemDTO): if not isinstance(item, ItemDTO) and not isinstance(item, dict):
LOG.debug(f"Received invalid item type '{type(item)}' with event '{event}'.")
return [] return []
tasks = [] tasks = []
@ -56,12 +60,13 @@ class Webhooks:
return await asyncio.gather(*tasks) return await asyncio.gather(*tasks)
async def __send(self, event: str, target: dict, item: ItemDTO) -> dict: async def __send(self, event: str, target: dict, item: ItemDTO | dict) -> dict:
from .config import Config from .config import Config
req: dict = target.get("request") req: dict = target.get("request")
itemId = item.get("id", item.get("_id", "??"))
try: try:
LOG.info(f"Sending event '{event}' id '{item.id}' to '{target.get('name')}'.") LOG.info(f"Sending event '{event}' id '{itemId}' to '{target.get('name')}'.")
async with httpx.AsyncClient() as client: async with httpx.AsyncClient() as client:
request_type = req.get("type", "json") request_type = req.get("type", "json")
@ -76,17 +81,17 @@ class Webhooks:
match request_type: match request_type:
case "json": case "json":
reqBody["json"] = item.__dict__ reqBody["json"] = item.__dict__ if isinstance(item, ItemDTO) else item
reqBody["headers"]["Content-Type"] = "application/json" reqBody["headers"]["Content-Type"] = "application/json"
case _: case _:
reqBody["data"] = item.__dict__ reqBody["data"] = item.__dict__ if isinstance(item, ItemDTO) else item
reqBody["headers"]["Content-Type"] = "application/x-www-form-urlencoded" reqBody["headers"]["Content-Type"] = "application/x-www-form-urlencoded"
response = await client.request(**reqBody) response = await client.request(**reqBody)
respData = {"url": req.get("url"), "status": response.status_code, "text": response.text} respData = {"url": req.get("url"), "status": response.status_code, "text": response.text}
msg = f"Webhook target '{target.get('name')}' Responded to event '{event}' id '{item.id}' with status '{response.status_code}'." msg = f"Webhook target '{target.get('name')}' Responded to event '{event}' id '{itemId}' with status '{response.status_code}'."
if Config.get_instance().debug and respData.get("text"): if Config.get_instance().debug and respData.get("text"):
msg += f" body '{respData.get('text','??')}'." msg += f" body '{respData.get('text','??')}'."
@ -94,7 +99,7 @@ class Webhooks:
return respData return respData
except Exception as e: except Exception as e:
LOG.error(f"Error sending event '{event}' id '{item.id}' to '{target.get('name')}'. '{e}'") LOG.error(f"Error sending event '{event}' id '{itemId}' to '{target.get('name')}'. '{e}'")
return {"url": req.get("url"), "status": 500, "text": str(e)} return {"url": req.get("url"), "status": 500, "text": str(e)}
def emit(self, event, data, **kwargs): def emit(self, event, data, **kwargs):

View file

@ -56,6 +56,8 @@ class Config:
db_file: str = "{config_path}/ytptube.db" db_file: str = "{config_path}/ytptube.db"
manual_archive: str = "{config_path}/archive.manual.log" manual_archive: str = "{config_path}/archive.manual.log"
ui_update_title: bool = True ui_update_title: bool = True
pip_packages: str = ""
pip_ignore_updates: bool = False
# immutable config vars. # immutable config vars.
version: str = APP_VERSION version: str = APP_VERSION
@ -146,6 +148,7 @@ class Config:
"remove_files", "remove_files",
"ignore_ui", "ignore_ui",
"ui_update_title", "ui_update_title",
"pip_ignore_updates",
) )
_frontend_vars: tuple = ( _frontend_vars: tuple = (

View file

@ -9,6 +9,29 @@ import operator
import os import os
# parameter-less decorator
def async_lru_cache_decorator(async_function):
@functools.lru_cache
def cached_async_function(*args, **kwargs):
coroutine = async_function(*args, **kwargs)
return asyncio.ensure_future(coroutine)
return cached_async_function
# decorator with options
def async_lru_cache(*lru_cache_args, **lru_cache_kwargs):
def async_lru_cache_decorator(async_function):
@functools.lru_cache(*lru_cache_args, **lru_cache_kwargs)
def cached_async_function(*args, **kwargs):
coroutine = async_function(*args, **kwargs)
return asyncio.ensure_future(coroutine)
return cached_async_function
return async_lru_cache_decorator
class FFProbeError(Exception): class FFProbeError(Exception):
pass pass
@ -163,78 +186,86 @@ class FFStream:
raise FFProbeError("None integer bit_rate") raise FFProbeError("None integer bit_rate")
class FFProbe: class FFProbeResult:
""" def __init__(self):
FFProbe wraps the ffprobe command and pulls the data into an object form:: self.metadata: dict = {}
metadata=FFProbe('multimedia-file.mov') self.video: list[FFStream] = []
""" self.audio: list[FFStream] = []
self.subtitle: list[FFStream] = []
self.attachment: list[FFStream] = []
audio: list[FFStream] = [] def get(self, key: str, default=None):
attachment: list[FFStream] = [] return getattr(self, key) if hasattr(self, key) else default
streams: list[FFStream] = []
subtitle: list[FFStream] = []
video: list[FFStream] = []
metadata: dict = {}
path_to_video: str = ""
def __init__(self, path_to_video): def streams(self) -> list[FFStream]:
self.path_to_video = path_to_video "List of all streams."
return self.video + self.audio + self.subtitle + self.attachment
async def run(self): def has_video(self):
try: "Is there a video stream?"
with open(os.devnull, "w") as tempf: return len(self.video) > 0
await asyncio.create_subprocess_exec("ffprobe", "-h", stdout=tempf, stderr=tempf)
except FileNotFoundError:
raise IOError("ffprobe not found.")
if not os.path.isfile(self.path_to_video): def has_audio(self):
raise IOError(f"No such media file '{self.path_to_video}'.") "Is there an audio stream?"
return len(self.audio) > 0
args = [ def has_subtitle(self):
"-v", "Is there a subtitle stream?"
"quiet", return len(self.subtitle) > 0
"-of",
"json",
"-show_streams",
"-show_format",
self.path_to_video,
]
p = await asyncio.create_subprocess_exec(
"ffprobe",
*args,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
exitCode = await p.wait()
data, err = await p.communicate()
if 0 == exitCode:
parsed: dict = json.loads(data.decode("utf-8"))
else:
raise FFProbeError(f"FFProbe return with non-0 exit code. '{err.decode('utf-8')}'")
stream = False
self.streams = []
self.video = []
self.audio = []
self.subtitle = []
self.attachment = []
for stream in parsed.get("streams", []):
self.streams.append(FFStream(stream))
self.metadata = parsed.get("format", {})
for stream in self.streams:
if stream.is_audio():
self.audio.append(stream)
elif stream.is_video():
self.video.append(stream)
elif stream.is_subtitle():
self.subtitle.append(stream)
elif stream.is_attachment():
self.attachment.append(stream)
def __repr__(self): def __repr__(self):
return "<FFprobe: {metadata}, {video}, {audio}, {subtitle}, {attachment}>".format(**vars(self)) return "<FFprobe: {metadata}, {video}, {audio}, {subtitle}, {attachment}>".format(**vars(self))
@async_lru_cache(maxsize=512)
async def ffprobe(file: str) -> FFProbeResult:
"""
Run ffprobe on a file and return the parsed data as a dictionary.
Args:
file (str): The path to the media file.
Returns:
dict: A dictionary containing the parsed data.
"""
try:
with open(os.devnull, "w") as tempf:
await asyncio.create_subprocess_exec("ffprobe", "-h", stdout=tempf, stderr=tempf)
except FileNotFoundError:
raise IOError("ffprobe not found.")
if not os.path.isfile(file):
raise IOError(f"No such media file '{file}'.")
args = ["-v", "quiet", "-of", "json", "-show_streams", "-show_format", file]
p = await asyncio.create_subprocess_exec(
"ffprobe",
*args,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
exitCode = await p.wait()
data, err = await p.communicate()
if 0 == exitCode:
parsed: dict = json.loads(data.decode("utf-8"))
else:
raise FFProbeError(f"FFProbe return with non-0 exit code. '{err.decode('utf-8')}'")
result = FFProbeResult()
result.metadata = parsed.get("format", {})
for stream in parsed.get("streams", []):
stream = FFStream(stream)
if stream.is_audio():
result.audio.append(stream)
elif stream.is_video():
result.video.append(stream)
elif stream.is_subtitle():
result.subtitle.append(stream)
elif stream.is_attachment():
result.attachment.append(stream)
return result

View file

@ -19,6 +19,7 @@ from library.encoder import Encoder
from library.HttpAPI import HttpAPI, LOG as http_logger from library.HttpAPI import HttpAPI, LOG as http_logger
from library.HttpSocket import HttpSocket from library.HttpSocket import HttpSocket
from library.Webhooks import Webhooks from library.Webhooks import Webhooks
from library.PackageInstaller import PackageInstaller
LOG = logging.getLogger("app") LOG = logging.getLogger("app")
MIME = magic.Magic(mime=True) MIME = magic.Magic(mime=True)
@ -37,6 +38,13 @@ class Main:
self.encoder = Encoder() self.encoder = Encoder()
self.checkFolders() self.checkFolders()
try:
PackageInstaller(self.config).check()
except Exception as e:
LOG.error(f"Failed to check for packages. Error message '{str(e)}'.")
LOG.exception(e)
caribou.upgrade(self.config.db_file, os.path.join(self.rootPath, "migrations")) caribou.upgrade(self.config.db_file, os.path.join(self.rootPath, "migrations"))
connection = sqlite3.connect(database=self.config.db_file, isolation_level=None) connection = sqlite3.connect(database=self.config.db_file, isolation_level=None)