Merge pull request #157 from arabcoders/dev
fix yt-dlp Impersonate target and webhook not firing
This commit is contained in:
commit
5301e134ce
13 changed files with 417 additions and 107 deletions
2
.vscode/launch.json
vendored
2
.vscode/launch.json
vendored
|
|
@ -30,6 +30,7 @@
|
|||
"console": "internalConsole",
|
||||
"justMyCode": true,
|
||||
"env": {
|
||||
"PYDEVD_DISABLE_FILE_VALIDATION": "1",
|
||||
"YTP_CONFIG_PATH": "${workspaceFolder}/var/config",
|
||||
"YTP_DOWNLOAD_PATH": "${workspaceFolder}/var/downloads",
|
||||
"YTP_TEMP_PATH": "${workspaceFolder}/var/tmp",
|
||||
|
|
@ -39,6 +40,7 @@
|
|||
"YTP_YTDL_DEBUG": "true",
|
||||
"YTP_MAX_WORKERS": "2",
|
||||
"YTP_IGNORE_UI": "true",
|
||||
"YTP_PIP_IGNORE_UPDATES": "true",
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
|||
3
Pipfile
3
Pipfile
|
|
@ -19,6 +19,9 @@ curl_cffi = "*"
|
|||
pysubs2 = "*"
|
||||
regex = "*"
|
||||
yt-dlp = "*"
|
||||
mutagen = "*"
|
||||
brotli = "*"
|
||||
brotlicffi = "*"
|
||||
|
||||
[dev-packages]
|
||||
|
||||
|
|
|
|||
176
Pipfile.lock
generated
176
Pipfile.lock
generated
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "d132a1657486171e8fddcc57ff4eb235886b74892116c54795c1d8122700e2f9"
|
||||
"sha256": "cd9a56440fe36b901993ced28ee6ef8d301a5125344cda86519fb7c54ede1449"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
|
|
@ -156,6 +156,171 @@
|
|||
"markers": "python_version >= '3.8'",
|
||||
"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": {
|
||||
"hashes": [
|
||||
"sha256:13a66fc9ef9b1c9e9ef220876d59a44ee5eff9e579655252271b7514fc0ad787",
|
||||
|
|
@ -549,6 +714,15 @@
|
|||
"markers": "python_version >= '3.8'",
|
||||
"version": "==6.1.0"
|
||||
},
|
||||
"mutagen": {
|
||||
"hashes": [
|
||||
"sha256:719fadef0a978c31b4cf3c956261b3c58b6948b32023078a2117b1de09f0fc99",
|
||||
"sha256:edd96f50c5907a9539d8e5bba7245f62c9f520aef333d13392a79a4f70aca719"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==1.47.0"
|
||||
},
|
||||
"propcache": {
|
||||
"hashes": [
|
||||
"sha256:03ff9d3f665769b2a85e6157ac8b439644f2d7fd17615a82fa55739bc97863f4",
|
||||
|
|
|
|||
|
|
@ -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_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_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
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import math
|
|||
import os
|
||||
from urllib.parse import quote
|
||||
from .Utils import calcDownloadPath, StreamingError
|
||||
from .ffprobe import FFProbe
|
||||
from .ffprobe import ffprobe
|
||||
|
||||
|
||||
class M3u8:
|
||||
|
|
@ -30,15 +30,14 @@ class M3u8:
|
|||
raise StreamingError(f"File '{realFile}' does not exist.")
|
||||
|
||||
try:
|
||||
ffprobe = FFProbe(realFile)
|
||||
await ffprobe.run()
|
||||
ff = await ffprobe(realFile)
|
||||
except UnicodeDecodeError:
|
||||
pass
|
||||
|
||||
if "duration" not in ffprobe.metadata:
|
||||
if "duration" not in ff.metadata:
|
||||
raise StreamingError(f"Unable to get '{realFile}' play duration.")
|
||||
|
||||
duration: float = float(ffprobe.metadata.get("duration"))
|
||||
duration: float = float(ff.metadata.get("duration"))
|
||||
|
||||
m3u8 = []
|
||||
|
||||
|
|
@ -53,7 +52,7 @@ class M3u8:
|
|||
|
||||
segmentParams: dict = {}
|
||||
|
||||
for stream in ffprobe.streams:
|
||||
for stream in ff.streams():
|
||||
if stream.is_video():
|
||||
if stream.codec_name not in self.ok_vcodecs:
|
||||
segmentParams["vc"] = 1
|
||||
|
|
|
|||
61
app/library/PackageInstaller.py
Normal file
61
app/library/PackageInstaller.py
Normal 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)
|
||||
|
|
@ -2,10 +2,8 @@ import glob
|
|||
import re
|
||||
from pathlib import Path
|
||||
from urllib.parse import quote
|
||||
|
||||
from aiohttp.web import Response
|
||||
|
||||
from .ffprobe import FFProbe
|
||||
from .ffprobe import ffprobe
|
||||
from .Subtitle import Subtitle
|
||||
from .Utils import calcDownloadPath, checkId, StreamingError
|
||||
|
||||
|
|
@ -32,15 +30,14 @@ class Playlist:
|
|||
)
|
||||
|
||||
try:
|
||||
ffprobe = FFProbe(rFile)
|
||||
await ffprobe.run()
|
||||
ff = await ffprobe(rFile)
|
||||
except UnicodeDecodeError:
|
||||
pass
|
||||
|
||||
if "duration" not in ffprobe.metadata:
|
||||
if "duration" not in ff.metadata:
|
||||
raise StreamingError(f"Unable to get '{rFile}' duration.")
|
||||
|
||||
duration: float = float(ffprobe.metadata.get("duration"))
|
||||
duration: float = float(ff.metadata.get("duration"))
|
||||
|
||||
playlist = []
|
||||
playlist.append("#EXTM3U")
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import logging
|
|||
import os
|
||||
import tempfile
|
||||
|
||||
from .ffprobe import ffprobe
|
||||
from .config import Config
|
||||
from .Utils import calcDownloadPath, StreamingError
|
||||
|
||||
|
|
@ -21,6 +22,7 @@ class Segments:
|
|||
self.acodec = config.streamer_acodec
|
||||
# sadly due to unforeseen circumstances, we have to convert the video for now.
|
||||
self.vconvert = True
|
||||
self.aconvert = True
|
||||
|
||||
async def stream(self, path: str, file: str) -> bytes:
|
||||
realFile: str = calcDownloadPath(basePath=path, folder=file, createPath=False)
|
||||
|
|
@ -28,6 +30,11 @@ class Segments:
|
|||
if not os.path.exists(realFile):
|
||||
raise StreamingError(f"File {realFile} does not exist.")
|
||||
|
||||
try:
|
||||
ff = await ffprobe(realFile)
|
||||
except UnicodeDecodeError:
|
||||
pass
|
||||
|
||||
tmpDir: str = tempfile.gettempdir()
|
||||
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("-1")
|
||||
|
||||
fargs.append("-pix_fmt")
|
||||
fargs.append("yuv420p")
|
||||
fargs.append("-g")
|
||||
fargs.append("52")
|
||||
# video section.
|
||||
if ff.has_video():
|
||||
fargs.append("-pix_fmt")
|
||||
fargs.append("yuv420p")
|
||||
fargs.append("-g")
|
||||
fargs.append("52")
|
||||
|
||||
fargs.append("-map")
|
||||
fargs.append("0:v:0")
|
||||
fargs.append("-strict")
|
||||
fargs.append("-2")
|
||||
fargs.append("-map")
|
||||
fargs.append("0:v:0")
|
||||
fargs.append("-strict")
|
||||
fargs.append("-2")
|
||||
|
||||
fargs.append("-codec:v")
|
||||
fargs.append(self.vcodec if self.vconvert else "copy")
|
||||
fargs.append("-codec:v")
|
||||
fargs.append(self.vcodec if self.vconvert else "copy")
|
||||
|
||||
# audio section.
|
||||
fargs.append("-map")
|
||||
fargs.append("0:a:0")
|
||||
fargs.append("-codec:a")
|
||||
fargs.append(self.acodec if self.aconvert else "copy")
|
||||
if ff.has_audio():
|
||||
fargs.append("-map")
|
||||
fargs.append("0:a:0")
|
||||
fargs.append("-codec:a")
|
||||
fargs.append(self.acodec if self.aconvert else "copy")
|
||||
|
||||
fargs.append("-sn")
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ from functools import lru_cache
|
|||
from typing import Any
|
||||
|
||||
import yt_dlp
|
||||
from yt_dlp.networking.impersonate import ImpersonateTarget
|
||||
|
||||
LOG = logging.getLogger("Utils")
|
||||
|
||||
|
|
@ -157,7 +158,16 @@ def mergeDict(source: dict, destination: 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 = (
|
||||
"cookiefile",
|
||||
|
|
@ -171,7 +181,12 @@ def mergeConfig(config: dict, new_config: dict) -> dict:
|
|||
if key in new_config:
|
||||
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]]:
|
||||
|
|
|
|||
|
|
@ -29,20 +29,24 @@ class Webhooks:
|
|||
LOG.info(f"Loading webhooks from '{file}'.")
|
||||
from .Utils import load_file
|
||||
|
||||
(target, status, error) = load_file(file, list)
|
||||
(targets, status, error) = load_file(file, list)
|
||||
if not status:
|
||||
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:
|
||||
LOG.error(f"Error loading webhooks from '{file}'. '{e}'")
|
||||
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:
|
||||
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 []
|
||||
|
||||
tasks = []
|
||||
|
|
@ -56,12 +60,13 @@ class Webhooks:
|
|||
|
||||
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
|
||||
|
||||
req: dict = target.get("request")
|
||||
itemId = item.get("id", item.get("_id", "??"))
|
||||
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:
|
||||
request_type = req.get("type", "json")
|
||||
|
||||
|
|
@ -76,17 +81,17 @@ class Webhooks:
|
|||
|
||||
match request_type:
|
||||
case "json":
|
||||
reqBody["json"] = item.__dict__
|
||||
reqBody["json"] = item.__dict__ if isinstance(item, ItemDTO) else item
|
||||
reqBody["headers"]["Content-Type"] = "application/json"
|
||||
case _:
|
||||
reqBody["data"] = item.__dict__
|
||||
reqBody["data"] = item.__dict__ if isinstance(item, ItemDTO) else item
|
||||
reqBody["headers"]["Content-Type"] = "application/x-www-form-urlencoded"
|
||||
|
||||
response = await client.request(**reqBody)
|
||||
|
||||
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"):
|
||||
msg += f" body '{respData.get('text','??')}'."
|
||||
|
||||
|
|
@ -94,7 +99,7 @@ class Webhooks:
|
|||
|
||||
return respData
|
||||
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)}
|
||||
|
||||
def emit(self, event, data, **kwargs):
|
||||
|
|
|
|||
|
|
@ -56,6 +56,8 @@ class Config:
|
|||
db_file: str = "{config_path}/ytptube.db"
|
||||
manual_archive: str = "{config_path}/archive.manual.log"
|
||||
ui_update_title: bool = True
|
||||
pip_packages: str = ""
|
||||
pip_ignore_updates: bool = False
|
||||
|
||||
# immutable config vars.
|
||||
version: str = APP_VERSION
|
||||
|
|
@ -146,6 +148,7 @@ class Config:
|
|||
"remove_files",
|
||||
"ignore_ui",
|
||||
"ui_update_title",
|
||||
"pip_ignore_updates",
|
||||
)
|
||||
|
||||
_frontend_vars: tuple = (
|
||||
|
|
|
|||
|
|
@ -9,6 +9,29 @@ import operator
|
|||
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):
|
||||
pass
|
||||
|
||||
|
|
@ -163,78 +186,86 @@ class FFStream:
|
|||
raise FFProbeError("None integer bit_rate")
|
||||
|
||||
|
||||
class FFProbe:
|
||||
"""
|
||||
FFProbe wraps the ffprobe command and pulls the data into an object form::
|
||||
metadata=FFProbe('multimedia-file.mov')
|
||||
"""
|
||||
class FFProbeResult:
|
||||
def __init__(self):
|
||||
self.metadata: dict = {}
|
||||
self.video: list[FFStream] = []
|
||||
self.audio: list[FFStream] = []
|
||||
self.subtitle: list[FFStream] = []
|
||||
self.attachment: list[FFStream] = []
|
||||
|
||||
audio: list[FFStream] = []
|
||||
attachment: list[FFStream] = []
|
||||
streams: list[FFStream] = []
|
||||
subtitle: list[FFStream] = []
|
||||
video: list[FFStream] = []
|
||||
metadata: dict = {}
|
||||
path_to_video: str = ""
|
||||
def get(self, key: str, default=None):
|
||||
return getattr(self, key) if hasattr(self, key) else default
|
||||
|
||||
def __init__(self, path_to_video):
|
||||
self.path_to_video = path_to_video
|
||||
def streams(self) -> list[FFStream]:
|
||||
"List of all streams."
|
||||
return self.video + self.audio + self.subtitle + self.attachment
|
||||
|
||||
async def run(self):
|
||||
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.")
|
||||
def has_video(self):
|
||||
"Is there a video stream?"
|
||||
return len(self.video) > 0
|
||||
|
||||
if not os.path.isfile(self.path_to_video):
|
||||
raise IOError(f"No such media file '{self.path_to_video}'.")
|
||||
def has_audio(self):
|
||||
"Is there an audio stream?"
|
||||
return len(self.audio) > 0
|
||||
|
||||
args = [
|
||||
"-v",
|
||||
"quiet",
|
||||
"-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 has_subtitle(self):
|
||||
"Is there a subtitle stream?"
|
||||
return len(self.subtitle) > 0
|
||||
|
||||
def __repr__(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
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ from library.encoder import Encoder
|
|||
from library.HttpAPI import HttpAPI, LOG as http_logger
|
||||
from library.HttpSocket import HttpSocket
|
||||
from library.Webhooks import Webhooks
|
||||
from library.PackageInstaller import PackageInstaller
|
||||
|
||||
LOG = logging.getLogger("app")
|
||||
MIME = magic.Magic(mime=True)
|
||||
|
|
@ -37,6 +38,13 @@ class Main:
|
|||
self.encoder = Encoder()
|
||||
|
||||
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"))
|
||||
|
||||
connection = sqlite3.connect(database=self.config.db_file, isolation_level=None)
|
||||
|
|
|
|||
Loading…
Reference in a new issue