Compare commits

...

406 Commits
v0.4 ... main

Author SHA1 Message Date
meeb c6acd5378c move TIME_ZONE set by env var from local_settings to settings, resolves #462 2024-02-02 05:51:20 +11:00
meeb e7788eb8fb
Merge pull request #450 from InterN0te/main-delete-files-on-disk
Following Delete files on disk #426
2024-01-17 22:31:09 +11:00
meeb e4e0b48c0b
Merge pull request #460 from skayred/main
Optimized source page and dashboard loading
2024-01-17 22:29:59 +11:00
Dmitrii Savchenko 3573c1187f
Optimized source page and dashboard loading 2024-01-16 11:57:00 +02:00
meeb b11b667aff
Merge pull request #452 from ShaneBridges1234/patch-1
Update other-database-backends.md
2024-01-03 16:06:17 +11:00
Yottatron 1b581aa4ba
Update other-database-backends.md
Correct table new in SQL for MariaDB column compression.
2024-01-01 09:16:04 -08:00
meeb 7384c00713 fix typo in sponsorblock categories, remove dupe metadata flags, related to #362 2023-12-12 16:28:26 +11:00
meeb 4fdd172b05 tidy up and pass a serialised list through to youtube.download_media, may help with #362 2023-12-12 14:40:23 +11:00
Someone 9c18115032
Merge branch 'meeb:main' into main-delete-files-on-disk 2023-12-11 15:41:50 +01:00
meeb 6853c1fa76 fix tests 2023-12-11 13:42:33 +11:00
administrator ed07073cf4 Revert "Removed non-pertinent source option"
This reverts commit 46ba2593a2.

Restore option
2023-12-11 03:38:37 +01:00
administrator af94b37ee6 Revert "Removed non-pertinent source option"
This reverts commit ad1d49a835.

t cherry-pick 46a43b9

Restore option
2023-12-11 03:38:14 +01:00
administrator ad1d49a835 Removed non-pertinent source option 2023-12-11 03:25:17 +01:00
administrator 46ba2593a2 Removed non-pertinent source option 2023-12-11 03:19:43 +01:00
administrator 46a43b968a Rework delete method to delete all files matching filename
Remove Source folder if checkbox 'remove media' is checked
2023-12-11 02:29:57 +01:00
administrator 805a0eefbd Merge branch 'delete-files-on-disk' of https://github.com/sparklesmcfadden/tubesync into main-delete-files-on-disk
Merge from sparklesmcfadden:delete-files-on-disk
2023-12-11 00:19:37 +01:00
meeb 3a87b5779e
Merge pull request #448 from InterN0te/main-addNfoInfo
Added season and episode tags in NFO to get Jellyfin displaying correctly
2023-12-10 17:58:04 +11:00
Someone f86e72aa92
Optimization of episode calculation 2023-12-09 23:13:28 +01:00
Someone f550e32b5e
Fix secondary sorting on video key 2023-12-09 19:33:59 +01:00
Someone 034d877d6a
Add season and episode tags in NFO test 2023-12-09 17:56:32 +01:00
Someone b9b702ab85
Add season and episode tags in NFO 2023-12-09 17:55:17 +01:00
meeb c159c24d15
Merge pull request #443 from depuhitv/patch-1
compress sync_media table for mariadb
2023-12-04 18:05:02 +11:00
depuhitv 6c9772d573 fixed grammar 2023-12-04 16:26:46 +11:00
depuhitv 45b8b3f65b
compress sync_media table for mariadb
Added steps to compress sync_media table for mariadb.
For 1,608 records, I am seeing the size reduced from 642.8 MB to 55.8 MB
2023-12-04 16:18:10 +11:00
meeb 7aa9c0ec8a bump to 0.13.3 2023-11-30 18:58:29 +11:00
meeb e54a762a7b rework skip logic check, prevent race condition between metadata downloading and upload date being checked, resolves #440, #183, related to #438 2023-11-30 18:52:32 +11:00
meeb 512b70adad toggle logging verbosity based on settings.DEBUG 2023-11-30 18:50:22 +11:00
meeb 6c21ff15ab stopcontainer helper 2023-11-30 18:49:58 +11:00
meeb adf26cb4e3 bump ffmpeg to autobuild-2023-11-29-14-19 2023-11-30 18:49:50 +11:00
meeb 45c12561ba
Merge pull request #438 from locke4/main
Fix signals.py mistake
2023-11-29 04:05:13 +11:00
locke4 2d6f485a5d Update signals.py 2023-11-28 08:48:31 +00:00
meeb 33b471175a
Merge pull request #425 from locke4/main
Add support for regex video title filtering
2023-11-20 16:53:58 +11:00
meeb 7f4e8586b7
Merge pull request #435 from klinker41/patch-1
Update other-database-backends.md
2023-11-20 16:51:00 +11:00
Jake Klinker bab4b9b056
Update other-database-backends.md
Add documentation about how to use a docker compose postgres container and connect it to tubesync. This seems like a fairly basic use case that many users would want to implement, given the large performance benefits it brings.
2023-11-19 10:23:07 -07:00
meeb 30c2127271 bump ffmpeg to 2023-11-14 and yt-dlp to 2023.11.16 2023-11-16 18:54:57 +11:00
locke4 d1cb7ef76c
Delete tubesync/sync/migrations/0020_auto_20231024_1812.py 2023-10-24 19:26:50 +01:00
locke4 1fd4f87c53
Merge pull request #8 from locke4/fix-pagenums
Ran makemigrations
2023-10-24 19:25:52 +01:00
locke4 cf06f4cbc2
Merge pull request #7 from locke4/locke4-patch-2
Updated according to comments on PR
2023-10-24 18:38:17 +01:00
locke4 0523f481d2 Updated according to comments on PR
Fixed whitespace

Update tests.py

Ran makemigrations

Update models.py

Update tests.py

Update models.py

Update tests.py

Update models.py

Update models.py

Update tests.py

Update models.py

Update tests.py

Update tests.py

Update tests.py

Update models.py

Update models.py

Update tests.py

Update models.py

Update models.py

Update tests.py

Update tests.py

Update signals.py

Update tasks.py

Update signals.py

Update models.py

Update tasks.py

Update signals.py

Update tasks.py

Update models.py
2023-10-24 18:37:09 +01:00
locke4 aa4bd4ec26 Ran makemigrations 2023-10-24 18:17:56 +01:00
locke4 96d9ee93ef
Merge pull request #6 from locke4/fix-pagenums
Fix pagenums for "only_skipped" query param
2023-10-22 13:39:11 +01:00
sparklesmcfadden 43cf532903 Adds option to delete files on disk when removing a media item from TubeSync 2023-10-21 20:54:21 -05:00
locke4 8240c49d5c
Update ci.yaml 2023-10-22 02:42:57 +01:00
locke4 0c5e3d3818
Update media.html 2023-10-22 02:30:24 +01:00
locke4 22edd1bbda
Update pagination.html 2023-10-22 02:25:19 +01:00
locke4 fea0bb191e
Fix typo 2023-10-21 21:23:57 +01:00
locke4 0f65a4027a Add support for regex filters on video names
Update views.py
Update tests.py
Update source.html
Update tasks.py
Update signals.py
Update 0001_initial.py
Update models.py
Update models.py
Update tests.py
2023-10-21 21:07:15 +01:00
meeb 5cac374486
Merge pull request #420 from sparklesmcfadden/delete-removed-media
Adds workflow to delete local media that no longer exists in the source
2023-10-21 14:31:39 +11:00
meeb 69efc9298d
Merge pull request #423 from ltomes/patch-1
Update other-database-backends.md
2023-10-21 14:30:06 +11:00
Levi Tomes 1be8dff769
Update other-database-backends.md
django-admin only ran the loaddata for me with the - before the format flag.
2023-10-20 18:22:40 -05:00
cavanfarrell 350e544594 Fixes formatting 2023-10-20 10:25:20 -05:00
cavanfarrell 0542c734e5 Adds workflow to delete local media that no longer exists in the source 2023-10-20 10:19:57 -05:00
meeb 42b337c408 bump ffmpeg to autobuild-2023-10-11-14-20 2023-10-12 15:50:38 +11:00
meeb 2f82f8c599 fix tests 2023-10-12 15:44:51 +11:00
meeb b57ca110b0 bump to 0.13.1 2023-10-12 15:34:33 +11:00
meeb e3e7352600 add uploader variable, resolves #270 2023-10-12 15:33:58 +11:00
meeb 6d3a7bf859 move metadata collection to a higher priority over thumbnails, resolves #418 2023-10-12 15:27:19 +11:00
meeb 25f622311f bump to 0.13.0 2023-09-25 18:47:21 +10:00
meeb adea4a0ecd bump ffmpeg to autobuild-2023-09-24-14-11 2023-09-25 18:45:45 +10:00
meeb 0d76f2f94e bump s6 to 3.1.5.0 2023-09-25 18:37:29 +10:00
meeb 71578d926e fix tests after subs lang pr 2023-09-25 18:31:32 +10:00
meeb 777cdb5ecc
Merge pull request #406 from pacoccino/subtitles
Subtitles
2023-09-05 06:33:26 +10:00
pacoccino 3dd445bf96 Add a validator for sub_lang 2023-09-04 14:58:57 +02:00
pacoccino 86744c0510 Remove extension edits 2023-09-02 14:37:07 +02:00
pacoccino be7454f72a Add subtitles config into sources model 2023-09-02 14:30:23 +02:00
pacoccino e9f03cb6bf download subtitles draft 2023-08-31 22:40:29 +02:00
meeb ddc127e6af bump libs, bump ffmpeg to autobuild-2023-08-12-14-12, resolves #399 2023-08-13 17:43:43 +10:00
meeb 63d32a1e11 replace PIL.Image.ANTIALIAS with PIL.Image.LANCZOS, resolves #392 2023-07-16 00:06:05 +10:00
meeb 2ebbb8480e bump ffmpeg to 2023-07-14-14-08 2023-07-15 14:32:16 +10:00
meeb 21785e031a add requests[socks], resolves #391 2023-07-15 14:23:23 +10:00
meeb f12e13162f ignore media formats which do not have acodecs or vcodecs in their respective matchers, resolves #386 2023-06-29 23:48:35 +10:00
meeb 5c9c1550bf make shell helper 2023-06-29 23:30:47 +10:00
meeb 12638afb60 bump container image base to debian bookworm, update ffmpeg to 2023-06-27 and yt-dlp to 2023-06-22, rework python packages installation after bookworm update 2023-06-28 02:54:09 +10:00
meeb b9886a3b27
Merge pull request #381 from a-kr/fix_cleanup_old_media
in cleanup_old_media, filter in database rather than in Python
2023-05-27 14:03:35 +10:00
Alexey Kryuchkov 612f78e7eb in cleanup_old_media, filter in database rather than in Python 2023-05-27 01:28:15 +03:00
meeb 0c5a9c53f8
Merge pull request #376 from gautamkrishnar/fix/dockerfile
fixing unavailable ffmpeg version
2023-05-04 12:38:27 +10:00
Gautam krishna R d439b2f223
fixing unavailable ffmpeg version
fixing unavailable ffmpeg version
2023-05-03 21:33:16 +05:30
meeb 7116617cd2
Merge pull request #374 from garbled1/latest_dl_fix
Fix #364 by checking the filesize is not null.
2023-05-03 02:02:14 +10:00
garbled1 422d228359 Fix #364 by checking the filesize is not null. 2023-05-02 08:24:50 -07:00
meeb 1f68be5c26 update ffmpeg to 2023-04-13-12-52 2023-04-14 12:28:38 +10:00
meeb 089a487f3a add additional library ID help link, resolves #370 2023-04-14 11:45:52 +10:00
meeb 24ae70ea70 add reset-metadata command, related to #287 2023-04-05 11:02:21 +10:00
meeb 72c3242e70 add TUBESYNC_RESET_DOWNLOAD_DIR env var to toggle resetting permissions on /downloads in the container on start, resolves #354 2023-03-26 14:05:47 +11:00
meeb f3e93c0ecf bump ffmpeg to autobuild-2023-03-23-15-58 2023-03-24 13:17:12 +11:00
meeb fa8efb178e allow easy container env var override of HEALTHCHECK_ALLOWED_IPS, resolves #168 2023-03-24 13:02:16 +11:00
meeb 2001faea44
Merge pull request #358 from darmiel/fix/font-weight
fix: bold font weight
2023-03-11 15:49:06 +11:00
darmiel b370e98031
fix: bold font weight 2023-03-10 14:36:29 +01:00
meeb 55bfd911b9 catch typeerrors for duration metadata, resolves #248 2023-03-10 18:23:49 +11:00
meeb e47d0eb7be
Merge pull request #357 from darmiel/fix/bump-ffpmeg
chore: bump ffmpeg to `109977-gaca7ef78cc`
2023-03-10 05:00:15 +11:00
darmiel a95c64bc10
chore: bump ffmpeg to `109977-gaca7ef78cc` 2023-03-09 14:58:41 +01:00
meeb e9d4f89f39 fix connection kwarg to db_type() in custom field to be compatible with the postgresql backend, resolves #347 2023-02-21 13:55:03 +11:00
meeb 7876b48860 use backend agnostic text type for custom field, related to #345 and #338 2023-02-20 14:56:28 +11:00
meeb 2639d911ab change sponsorblock_categories to a textfield, fixing max charlen=255 for mysql, related to #338 2023-02-20 13:24:38 +11:00
meeb e4c0f0e98a
Merge pull request #338 from kuhnchris/embed-thumbnail
Configurations in Sources
2023-02-20 11:23:18 +11:00
KuhnChris 601449ce08 migrate migrations; split fields into `fields.py` 2023-02-19 23:44:48 +01:00
KuhnChris fe4c876fdc "source" overview, fix some edge case(s) 2023-02-18 14:03:32 +01:00
KuhnChris fbe9546a74 Merge branch 'meeb-main' into embed-thumbnail 2023-02-18 11:38:59 +01:00
KuhnChris ce14167cee formating 2023-02-18 11:38:23 +01:00
KuhnChris c927f32aa6 ffmpeg embed thumbnails, configuration 2023-02-18 11:37:28 +01:00
KuhnChris 1d5579aa31 Phase 1 - extend model for new fields 2023-02-18 11:35:45 +01:00
meeb d8a9572411 bump ffmpeg, fix container build 2023-02-18 13:14:25 +11:00
meeb 8315efac03 bump to 0.12.1, resolves #340 and #341 2023-02-18 12:59:57 +11:00
meeb 35678e3be9 temporarily disable sponsorblock by default pending #338 2023-02-18 12:54:39 +11:00
meeb e75b446883
Merge pull request #334 from kuhnchris/skip-manual
Adding new "manual_skip" field; adapt UI
2023-02-18 12:18:00 +11:00
meeb dd05595558
Merge pull request #337 from kuhnchris/align-checkboxtext
align (i) better with text (+ checkbox less wide)
2023-02-15 11:56:56 +11:00
KuhnChris 2772e85d9f ffmpeg embed thumbnails, configuration 2023-02-15 00:01:44 +01:00
KuhnChris 931aa78815 align (i) better with text (+ checkbox less wide) 2023-02-14 22:06:15 +01:00
KuhnChris 24a49d2f14 Phase 1 - extend model for new fields 2023-02-14 21:52:50 +01:00
meeb f14d2dd29e
Merge pull request #335 from kuhnchris/delete_index_source
del `delete_index_source_task` calls - solves #333
2023-02-14 18:55:57 +11:00
KuhnChris f4e5b6e76c del `delete_index_source_task` calls - solves #333 2023-02-13 14:23:41 +01:00
KuhnChris 977f996d8e Adding new "manual_skip" field; adapt UI 2023-02-13 07:46:16 +01:00
meeb dc5491455c
Merge pull request #331 from kuhnchris/sync-now-v2
"Sync now" button
2023-02-13 11:17:46 +11:00
meeb 70ef11d552
Merge pull request #332 from kuhnchris/patch-1
Remove automatic builds on pull_request
2023-02-13 11:16:11 +11:00
KuhnChris b04e237cb8
Remove automatic builds on pull_request
Removes the automatic pull request build due to failing for the regular contributors (aside from @meeb).
2023-02-12 19:14:41 +01:00
KuhnChris 55c58b4836 "Sync now" button 2023-02-12 19:03:28 +01:00
meeb e871983707
Merge pull request #325 from kuhnchris/allow-audio-play
Fix bug getting content_type for "audio only"
2023-02-12 23:51:37 +11:00
meeb b3f93ddef7
Merge pull request #327 from kuhnchris/dev-file-plrovider
(dev only) allow file download directly from django
2023-02-12 23:46:24 +11:00
KuhnChris bf7a0fcec0 content_type based on vcodec/acodec 2023-02-12 13:22:57 +01:00
KuhnChris 598ee2bd0a use pathlib; .exists() check 2023-02-12 13:09:33 +01:00
meeb 7b12fe3fad
Merge pull request #326 from kuhnchris/audio-player-main
<audio>-player instead of <video> for audio only
2023-02-12 12:02:01 +11:00
meeb 7358b52184
Merge pull request #324 from kuhnchris/suspicious-file-op
Add check against folders outside of DOWNLOAD_ROOT
2023-02-12 11:54:57 +11:00
meeb 4b4b4eb58d
Merge pull request #323 from kuhnchris/update-readme
Update README-FAQ regarding `Locking Failed`
2023-02-12 11:53:38 +11:00
KuhnChris b719fd5122 Allow file fetching directly from django (dev-env) 2023-02-11 22:07:48 +01:00
KuhnChris 4696aebebc allow the use of <audio> if only audio available. 2023-02-11 22:05:36 +01:00
KuhnChris 7d333487fe Fix bug getting content_type for "audio only" 2023-02-11 22:01:01 +01:00
KuhnChris 844d17006e Add check against folders outside of DOWNLOAD_ROOT 2023-02-11 20:42:59 +01:00
KuhnChris f9a27eb33e Update README-FAQ regarding `Locking Failed` 2023-02-11 19:16:25 +01:00
meeb b8434ff444
Merge pull request #320 from ticoombs/main
feat: initial sponsorblock support
2023-02-03 17:50:48 +11:00
Tim Coombs 932eb4caf4 feat: sponsorblock support 2023-02-03 15:18:52 +11:00
meeb 812fbc5f46 remove per-init timeout 2023-01-20 13:55:54 +11:00
meeb fdc591cc7c fix in-client video player with correct content type headers 2023-01-19 13:29:37 +11:00
meeb 4ae454a4f3 disable s6 service timeout entirely, resolves #309 2023-01-19 12:57:09 +11:00
meeb 4f6af702ae set S6_CMD_WAIT_FOR_SERVICES_MAXTIME to 60s 2023-01-19 11:38:01 +11:00
meeb 2431f8775a bump to 0.12.0 2023-01-19 05:17:50 +11:00
meeb 438316953a increase tubesync-init up timeout to 60s 2023-01-19 04:11:10 +11:00
meeb 85637fecba fix ci build 2023-01-18 18:45:34 +11:00
meeb f9dfffe91a
Merge pull request #305 from biolds/misc
Misc fixes
2023-01-18 18:41:01 +11:00
meeb 0845a6662d switch to s6 v3, bump s6 to 3.1.2.1, bump ffmpeg to 2023-01-03-12-55, bump yt-dlp to 2023.01.06, fix multi-arch builds 2023-01-18 18:39:23 +11:00
Laurent DEFERT 419c4c5a9f source edition refactoring 2023-01-17 21:34:41 +01:00
Laurent DEFERT 2f475bf2a8 limit the number of videos to process 2023-01-15 18:37:00 +01:00
Laurent DEFERT 7d16a1714c add missing migration 2023-01-15 18:37:00 +01:00
Laurent DEFERT a7100a0f53 prevent exceptions when metadata loading failed 2023-01-15 18:37:00 +01:00
Laurent DEFERT 5a4e6cee58 typo fix 2023-01-08 11:41:30 +01:00
Laurent DEFERT e69adafcec fix deleting media files 2022-12-28 18:38:38 +01:00
meeb f9908a4d3b
Merge pull request #297 from biolds/embed-video
Embedded video player
2022-12-28 22:41:08 +11:00
Laurent DEFERT bf99241ad2 embedded video player, video downloads 2022-12-28 12:03:40 +01:00
Laurent DEFERT 0e278bc8c4 fix relative media_file path
FileField should store a relative path, to make their "url" attribute work
2022-12-28 12:03:34 +01:00
meeb 57921ca6b9
Merge pull request #281 from PaulWoitaschek/patch-1
Specify the full compose syntax
2022-11-02 07:57:52 +11:00
Paul Woitaschek fb23fdeae1
Specify the full compose syntax 2022-11-01 18:02:29 +01:00
meeb 433a7792d5
Merge pull request #275 from serjs/ffmpeg
Change ffmpeg from ytdlp
2022-10-14 21:27:27 +11:00
Sergey Bogatyrets e198cc011b Change ffmpeg from ytdlp 2022-10-13 13:25:06 +03:00
meeb 296a790af5 bump libs 2022-09-29 03:19:12 +10:00
meeb e190821b7b bump libs 2022-09-24 16:56:41 +10:00
meeb 1ba865cf0d run build before test so static file tests work without precondition 2022-09-04 11:58:13 +10:00
meeb 05d50c958e add support for m.youtube.com as a netloc when validating source urls, resolves #264 2022-09-04 11:57:15 +10:00
meeb 8426c7309a bump libs 2022-09-04 11:46:04 +10:00
meeb 0450d47d81 bump libs 2022-08-25 17:01:39 +10:00
meeb e8d899d273 use container config base dir for cookies when using container local settings, resolves #259 2022-08-14 15:43:49 +10:00
meeb 25d5768f6e bump libs 2022-08-14 15:43:06 +10:00
meeb e9a3f2dd59 url prefix override typo in some environments, related to #255 2022-07-25 16:37:47 +10:00
meeb 7832282545 patch the wsgi application environ to support sub-URLs, add a master ENV var to set a sub-URL, tweak SASS and README to match, actually resolves #255 2022-07-25 13:36:12 +10:00
meeb d161aef112 allow Django STATIC_URL to be set, resolves #255 2022-07-24 17:51:46 +10:00
meeb 8901aea8d7 Merge branch 'main' of github.com:meeb/tubesync 2022-07-24 17:38:54 +10:00
meeb 227cae4cdb
Merge pull request #256 from rstrom1763/main
Correcting various spelling and grammar errors
2022-07-23 17:40:46 +10:00
Ryan 5e57abe86a Correcting various spelling and grammar errors
Corrected various spelling and grammar errors on the README.md file. Utilized a spell checker to verify.
2022-07-23 00:22:05 -05:00
meeb c04c1b3cfb bump libs 2022-07-20 17:45:10 +10:00
meeb a94541a354 replace all whitespaec with spaces in filenames, related to #35 2022-07-17 13:45:40 +10:00
meeb 84a368aa09 bump libs 2022-07-17 13:35:00 +10:00
meeb 6d2fb86e7d bump libs 2022-07-06 11:27:52 +10:00
meeb 67a3998aac bump to 0.11.0 2022-06-29 17:17:58 +10:00
meeb e3ca39b5db bump libs 2022-06-29 17:14:19 +10:00
meeb 872bfc5124 remove vague py version requirement, bump libs 2022-06-18 23:40:02 +10:00
meeb ae5550a28d add arm64 note 2022-06-09 00:00:00 +10:00
meeb 153ca032b1 bump libs 2022-06-08 23:57:37 +10:00
meeb 95e727b0a8
Merge pull request #246 from SweetMNM/main
Build docker image for amd64 and arm64
2022-06-08 23:37:49 +10:00
SweetMNM f1c6fc3086 🎪 Update CI action to support arm64 2022-06-08 08:20:08 +03:00
SweetMNM a3559526cb 🌵 Fix docker cache url 2022-06-08 08:13:36 +03:00
SweetMNM a0ca2b3061 🎪 Fix gchr username error 2022-06-07 21:59:47 +03:00
SweetMNM 120a19d2ba 🚜 Fix ghcr url in release action 2022-06-07 21:45:29 +03:00
SweetMNM 4735e72f12 ♻️ Retry with changes from BigCheeZ/tubesync
https://github.com/BigCheeZ/tubesync/tree/aarch64_support
2022-06-07 20:51:48 +03:00
SweetMNM 5954dba48d 🐳 Add piwheels pip source for arm64 2022-06-07 20:28:46 +03:00
SweetMNM 3f699c82ec 🎭 Fix dockerfile error 2022-06-07 19:49:29 +03:00
SweetMNM cb39ece21b 🐛 aarch64 test number 3 2022-06-07 19:41:11 +03:00
SweetMNM 3943115b18 🏁 Fix docker build 2022-06-07 19:33:03 +03:00
SweetMNM 97183fff97 Build docker image for aarch64 2022-06-07 19:04:34 +03:00
meeb b4a247bf37 bump libs 2022-06-03 22:44:41 +10:00
meeb 3bee755eb5
Merge pull request #244 from meeb/dependabot/pip/pillow-9.1.1
Bump pillow from 9.1.0 to 9.1.1
2022-06-03 22:43:01 +10:00
dependabot[bot] 9957639be5
Bump pillow from 9.1.0 to 9.1.1
Bumps [pillow](https://github.com/python-pillow/Pillow) from 9.1.0 to 9.1.1.
- [Release notes](https://github.com/python-pillow/Pillow/releases)
- [Changelog](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst)
- [Commits](https://github.com/python-pillow/Pillow/compare/9.1.0...9.1.1)

---
updated-dependencies:
- dependency-name: pillow
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-06-03 12:41:27 +00:00
meeb a5a8e37a20 bump libs 2022-05-16 15:01:44 +10:00
meeb 7a1b2adc59
Merge pull request #238 from Nedlinin/patch-1
Update dashboard.html
2022-05-14 12:11:03 +10:00
Chris Heath 7668466bc3
Update dashboard.html
Small typo fix of "Runtime infomation" -> "Runtime information"
2022-05-13 15:12:38 -05:00
meeb ceb8cbc442 bump libs 2022-05-01 14:15:46 +10:00
meeb 8b0d1b3397 bump libs 2022-04-23 13:29:35 +10:00
meeb 77fb4963f9 add a media filter to show only skipped media, resolves #197 2022-04-06 16:32:58 +10:00
meeb 538b3cb319 if media is downloaded use the downloaded filename and not generated filenames based on metadata parameters for addtional metadata files, resolves, resolves #67, resolves #83, resolves #204 2022-04-06 16:24:53 +10:00
meeb 2335ceb2dc bump libs 2022-04-06 16:03:10 +10:00
meeb 0c347d523d delete metadata when a media item is reset, related to #226 2022-03-28 19:14:14 +11:00
meeb d0a214e21b bump libs 2022-03-26 15:19:17 +11:00
meeb 2d8e6ed9b8 default TUBESYNC_HOSTS to *, resolves #224 2022-03-18 16:07:06 +11:00
meeb d0fcc07656 bump libs 2022-03-18 00:34:13 +11:00
meeb 5bf53b3d3a bump libs 2022-03-09 20:32:17 +11:00
meeb 280112beae bump libs 2022-02-28 00:34:00 +11:00
meeb 367d41f2be fix tests 2022-02-15 17:44:13 +11:00
meeb 61cd63bcc1 account for duration in metadata that is set, but has a null value 2022-02-15 17:40:43 +11:00
meeb 62e2e2f9e6 bump libs 2022-02-15 17:38:23 +11:00
meeb aa90a1afb0 bump libs 2022-02-03 18:03:00 +11:00
meeb 238c0b5911
Merge pull request #211 from ThibaultNocchi/add_write_info_json
Add possibilty to use yt dlp write-info-json flag
2022-02-03 15:10:27 +11:00
Thibault Nocchi 4d7e9133e0 add possibilty to use yt dlp write_info_json flag 2022-02-01 20:26:57 +01:00
meeb 709b7b44d5 bump libs 2022-01-28 20:37:25 +11:00
meeb 425b011054 spacing 2022-01-22 16:17:26 +11:00
meeb b1b3c99726 add cookies guide to readme 2022-01-22 16:16:31 +11:00
meeb 02212b8fad add support for exported cookies via cookies.txt, resolves #129 2022-01-22 16:13:46 +11:00
meeb 70e541dea0 bump libs 2022-01-22 16:13:00 +11:00
meeb cc7b7727c2 expose 4848 in Dockerfile, resolves #205 2022-01-17 12:42:08 +11:00
meeb 0757c99f01 doc tweaks, feedback from #195 2022-01-09 12:24:55 +11:00
meeb 61d97201a5 bump libs 2022-01-08 17:47:10 +11:00
meeb a58aef29fb bump libs 2022-01-01 18:59:38 +11:00
meeb 56c882fa79 bump libs 2021-12-20 15:42:10 +11:00
meeb 9a3030543f bump libs 2021-12-15 21:54:03 +11:00
meeb 4eca23d88b account for removed media having no published data in metadata, resolves #191 2021-12-10 14:57:50 +11:00
meeb aa6df98927 bump libs 2021-12-10 14:56:05 +11:00
meeb f3cac1908c pin django to 3.2.* 2021-12-07 21:02:48 +11:00
meeb d9a519ffde bump libs 2021-12-07 20:57:55 +11:00
meeb 185823b040 bump libs 2021-11-30 17:06:19 +11:00
meeb 4774a35d44 add non-persistent redis server to container, resolves #186 2021-11-16 22:20:16 +11:00
meeb b4a89968d0 bump libs 2021-11-16 16:25:28 +11:00
meeb 5056419aa4 bump libs 2021-11-11 15:42:21 +11:00
meeb a8488026d0 bump libs 2021-11-07 14:51:02 +11:00
meeb 6459e273f1 bump yt-dlp 2021-10-25 15:16:35 +11:00
meeb 42e4ee775f switch from depreciated etree iterator to iter, resolves #177 2021-10-19 14:16:06 +11:00
meeb b3d9e74818 revert 3c1d64a089, remove {hh} and {min} filepath macros, resolves #133 2021-10-17 21:35:08 +11:00
meeb c396821cb1 bump libs 2021-10-17 21:32:01 +11:00
meeb f9858a4d1a drop minimum video resolution that will be downloaded in SD to 240p from 360p, resolves #162 2021-10-15 21:04:53 +11:00
meeb 3c1d64a089 add {HH} for hour and {min} for minutes to output media format path formats, part of #133 2021-10-15 20:53:06 +11:00
meeb 00fbd53b11 remove static ffmpeg external binary, use Debian packaged ffmpeg, resolves #174 2021-10-15 20:35:28 +11:00
meeb 99825c9a08 switch container base to Debian bullseye, resolves #175 2021-10-15 20:18:03 +11:00
meeb 4f163f2f2c fix paths in Makefile 2021-10-15 20:17:26 +11:00
meeb 936800992c bump libs 2021-10-15 20:03:34 +11:00
meeb 2e9ee04c97 bump libs 2021-10-12 12:24:42 +11:00
meeb 8d60629034 bump libs 2021-10-09 16:59:23 +11:00
meeb f54adab213 bump libs 2021-10-04 17:58:08 +11:00
meeb 6618409f9c bump libs 2021-09-29 19:27:14 +10:00
meeb 8d08027024 add migrations for #158 2021-09-24 15:55:10 +10:00
meeb 9a543b1496 bump media_file maximum path to 255 characters, resolves #158 2021-09-24 15:53:33 +10:00
meeb b70703b7a7 patch youtube-dl-info command to work with yt-dlp metadata 2021-09-24 12:30:59 +10:00
meeb 6ac0c6e9de bump libs 2021-09-23 14:58:43 +10:00
meeb ecb1aaf5b5 disable hacky db connection keep-alive for postgresql, resolves #135 2021-09-21 23:11:31 +10:00
meeb 4c5027e0c4 swap youtube-dl to yt-dlp in README 2021-09-20 20:23:53 +10:00
meeb e8d75a79c5 fix release build 2021-09-20 17:27:39 +10:00
meeb ff4be7cfa0 fix release build 2021-09-20 17:14:59 +10:00
meeb c1cb19259e bump to 0.10.0 2021-09-20 17:08:22 +10:00
meeb 837b6c3107 switch to yt-dlp with required library updates, resolves #145 2021-09-20 16:49:03 +10:00
meeb ced6314a62 bump libs 2021-09-20 16:25:01 +10:00
meeb bb6c195ae7 bump libs 2021-09-20 00:18:52 +10:00
meeb c280b76777 bump libs 2021-09-11 14:58:35 +10:00
meeb 248da767b0 bump libs 2021-09-08 15:19:02 +10:00
meeb 1069b87295 remove outdated image version from readme 2021-09-04 20:09:39 +10:00
meeb 3525a65cd6 add webp support, part of Pillow lib upgrade, discussed in #150 2021-09-04 19:06:23 +10:00
meeb c51a5bb365 bump libs 2021-09-04 19:00:31 +10:00
meeb 7f4b9aff14 add libjpeg to container build, resolves #149 2021-09-04 17:13:21 +10:00
meeb a59e7fe65f remove broken playlist_index support in media formatting options, resolves #142 2021-09-04 15:22:45 +10:00
meeb 3e0a71f2ef add cli commands to list and delete sources, useful for deleting massive sources that currently time out as discussed in #148 2021-09-04 15:00:23 +10:00
meeb 3dfbca2af4 update container build to support pillow>=3, add webp image support 2021-09-03 14:46:13 +10:00
meeb 0c256f59d8 bump libs 2021-09-03 14:32:31 +10:00
meeb dbbae72c25 add yt-dlp to build 2021-09-03 14:31:17 +10:00
meeb b1b852d82c add check for stale PID file or old processes to gracefully handle hard container stop/starts, discussed in #144 2021-08-27 17:13:25 +10:00
meeb 437bb17f75 bump libs 2021-08-27 04:06:29 +10:00
meeb fdfcb5fd33 bump libs 2021-08-14 01:16:21 +10:00
meeb ff35f791f6 handle media having no metadata at all gracefully, resolves #138 2021-08-07 03:01:44 +10:00
meeb b2ea37ffec bump libs 2021-08-03 15:53:41 +10:00
meeb d89530d5b8
Merge pull request #136 from hangrymuppet/patch-1
Add libpq5 to docker image
2021-08-03 15:45:56 +10:00
H Bajaj f00050008b
Add libpq5 to docker image 2021-08-02 22:29:10 -07:00
meeb 68604d19c7 bump libs 2021-07-22 18:09:27 +10:00
meeb 55e5b5632f bump libs 2021-07-11 17:11:17 +10:00
meeb 5e18cb92dd bump libs 2021-07-04 16:05:55 +10:00
meeb 6178e0baa0 bump libs 2021-06-28 17:39:34 +10:00
meeb 8050bac507 add libpq-dev to container build 2021-06-19 15:36:39 +10:00
meeb 6dcdac1647 add postgresql-common to container build for new postgresql-binary 2021-06-19 15:30:59 +10:00
meeb 763f6b89ef bump libs 2021-06-19 15:22:19 +10:00
meeb 6c28292918 bump youtube-dl 2021-06-11 16:21:24 +10:00
meeb 574fc55a5e bump django 2021-06-04 14:03:39 +10:00
meeb c8fd74b3a4 Merge branch 'main' of github.com:meeb/tubesync into main 2021-06-02 15:13:00 +10:00
meeb 6622e17a5a bump libs 2021-06-02 15:12:50 +10:00
meeb ea05bd0b13
Merge pull request #125 from Bad3r/main
README.md - Line: 15 - Spelling Error
2021-06-02 15:08:30 +10:00
Bad3r 019c98dc76
Spelling Error
implemenations --> implementations
2021-06-01 18:26:08 -04:00
meeb 72dfe51a46 use env vars in image names and tags 2021-05-27 20:49:48 +10:00
meeb 22cebba8ac remove explicit Dockerfile path 2021-05-27 20:33:52 +10:00
meeb d51d198f94 remove $ in yaml docker tags 2021-05-27 20:28:50 +10:00
meeb ed0c2d7dd3 switch to actions v2 format 2021-05-27 20:26:11 +10:00
meeb 5ced901ae8 streamline multi-arch build, part of #81 2021-05-27 20:12:02 +10:00
meeb afda481046 custom builder just building amd64 for now pending multi-arch ffmpeg releases being available, part of #81 2021-05-27 20:01:29 +10:00
meeb a986864f77 add custom docker builder, part of #81 2021-05-27 19:56:21 +10:00
meeb ad1c4ecbc9 add multi-platform container image builds to latest image build process, part of #81 2021-05-27 19:49:41 +10:00
meeb 54b7de4442 add multi-platform container image builds to ci process, part of #81 2021-05-27 19:48:11 +10:00
meeb d1996aee80 add addmetadata as a default youtube-dl flag, part of #111 2021-05-27 19:22:56 +10:00
meeb 326cefbec1 add hack to force database connection to stay alive by hooking into DatabaseWrapper.ensure_connection, resolves #121 2021-05-27 19:12:29 +10:00
meeb d6e81c6af7 tweak can_download evaluation logic so post-metadata checks trigger a save if required, part of #108 2021-05-27 18:57:43 +10:00
meeb a000f8f2c0 update libs 2021-05-27 18:43:24 +10:00
meeb cbab09e931 bump libs 2021-05-22 03:01:29 +10:00
meeb 414fca08ca bump libs 2021-05-21 01:44:27 +10:00
meeb 874c71b7aa add mysql utf8mb4 encoding note to docs, resolves #117 2021-05-17 15:28:43 +10:00
meeb 5b101825f5 update tests 2021-05-17 01:30:04 +10:00
meeb 0db8db4351 force mysql to use utf8mb4 for connections, related to #117 2021-05-17 01:27:50 +10:00
meeb d4fd148089 update django 2021-05-15 15:00:20 +10:00
meeb c739d594d8 update libs 2021-05-06 23:10:45 +10:00
meeb 05e8ad8e89 update libs 2021-05-05 15:49:59 +10:00
meeb 024ab72e5f update libs 2021-05-04 14:25:09 +10:00
meeb 66ec3a29ec update tests to match changes in #115 2021-04-30 14:03:36 +10:00
meeb 28a565737f set database connections CONN_MAX_AGE to a default of 300, resolves #115 2021-04-30 14:00:04 +10:00
meeb 2c7116f6ba fix AutoField warning in django >= 3.2, part of #115 2021-04-30 13:55:33 +10:00
meeb 9ccb9db6de add missing libmariadb3 package, update gunicorn package hash, resolves #113 2021-04-28 23:20:21 +10:00
meeb 2d992cbb90 update libs 2021-04-26 13:17:05 +10:00
meeb 302a3614cf update libs 2021-04-25 13:32:48 +10:00
meeb ea546013de bump libs 2021-04-21 15:43:08 +10:00
meeb fb18610893 bump libs 2021-04-07 21:00:38 +10:00
meeb 2364432088 bump libs 2021-04-06 11:31:36 +10:00
meeb 655bed14fd fix loaddata import example in docs 2021-04-05 23:59:48 +10:00
meeb 721399f665 fix container build deps for python mysql client, related to #72 2021-04-05 00:31:50 +10:00
meeb 694ed5c581 update example database settings 2021-04-04 23:09:05 +10:00
meeb a98f2462ed tweak docs 2021-04-04 23:06:44 +10:00
meeb 5461a5357d check dicts output of parse_database_connection_string() in tests, part of #72 2021-04-04 23:04:51 +10:00
meeb 20df9f4044 support external postgresql, mysql and mariadb databases, resolves #72 2021-04-04 23:01:15 +10:00
meeb 3ec4f7c525 fix test running in Makefile 2021-04-04 22:16:22 +10:00
meeb 443fb827d0 update libs 2021-04-02 21:28:14 +11:00
meeb a810303f52 bump libs 2021-03-30 11:54:01 +11:00
meeb 9370a481f9
Merge pull request #100 from micahmo/main
Related to #88: Fix 4320p in test data.
2021-03-18 12:19:45 +11:00
Micah Morrison 1478c95d59 Related to #88: Fix 4320p in test data.
RESOLUTION_MAP expects lowercase p in resolution name.
2021-03-17 17:19:15 -04:00
meeb f69fa747af fix download cap date comparison check in signal, resolves #97 2021-03-15 01:13:16 +11:00
meeb a29a92893f
Merge pull request #95 from micahmo/main
Fix meeb/tubesync#90: Match media on source and id
2021-03-13 23:05:45 +11:00
Micah Morrison 7d471056c1 Fix meeb/tubesync#90: Match media on source and id 2021-03-11 20:37:44 -05:00
meeb 119493c181 disable warning on skipping an already skipped video with no publish date, related to #77 2021-03-08 13:26:34 +11:00
meeb 02a0f924b4 typo 2021-03-08 13:08:38 +11:00
meeb 38665eb00d add a secondary check when download tasks are triggered for download caps, related to #77 2021-03-08 12:20:44 +11:00
meeb c32358bcef media requires a published date, update tests to match, related to #77 2021-03-08 12:08:10 +11:00
meeb df9316bede handle media which may not have a published date set for some erroneous reason, related to #77 2021-03-08 11:55:23 +11:00
meeb 8525d920a0 update ffmpeg to 4.3.2, resolves #75 2021-03-07 17:38:35 +11:00
meeb a6e08d9a10 update s6-init to 2.2.0.3, resolves #76 2021-03-07 17:12:40 +11:00
meeb 2e0d0385b0 recalculate media skip flag when source download cap is updated, resolves #77 2021-03-07 14:03:19 +11:00
meeb 972c184c70 add longer source indexing options and a never option, resolves #68 2021-03-07 13:39:49 +11:00
meeb adeafbfcb4 update libs 2021-03-07 13:10:10 +11:00
meeb 2c1c45e829 bump to 0.9.1 2021-03-05 14:20:23 +11:00
meeb c64f54bcb4 bump to 0.9.1 2021-03-05 14:16:17 +11:00
meeb 6ce55b0337 bump libs 2021-03-04 15:39:51 +11:00
meeb d06c4beae0 bump upstream libs 2021-02-22 19:22:30 +11:00
meeb db651e16b9 raise a manual exception when youtube-dl extract_info returns no data to trigger backoffs, increase backoff retry and timers, resolves #66 2021-02-22 13:24:11 +11:00
meeb 86068790ed missing import 2021-02-21 11:52:14 +11:00
meeb ea72671351 add version details for http basic auth instructions 2021-02-21 11:45:56 +11:00
meeb 96b9eddf43 add a reset tasks cli command 2021-02-21 11:44:52 +11:00
meeb bceefc8b01 skip media which has no publish date in locally stored metadata 2021-02-21 11:15:57 +11:00
meeb 820cc69937 typo 2021-02-19 14:40:41 +11:00
meeb 1e8711be51 add media items downloaded counter to sources list overview 2021-02-19 14:37:56 +11:00
meeb e3423bc2d2 preserve media filter when toggling skipped media 2021-02-19 14:26:01 +11:00
meeb 6fbf72d0e7 optional basic HTTP authentication, resolves #62 2021-02-19 12:58:34 +11:00
meeb d6852bf828 account for metadata loading as None for upload_date, resolves #59 2021-02-18 20:50:13 +11:00
meeb f6f4f244d7 hide skipped media by default and add a show skipped media button 2021-02-18 19:34:44 +11:00
meeb df35aa2a5f increase media per page, tweak pagination button layout 2021-02-18 19:21:41 +11:00
meeb 799c0fce39 bump to v0.9 2021-02-18 17:29:03 +11:00
meeb 2f324f28a9 add sync-missing-metadata command with docs, resolves #25 2021-02-18 17:24:14 +11:00
meeb 895bfe6f87 scrub % in titles, resolves #55 2021-02-18 16:35:10 +11:00
meeb e0669b107d change default filename date prefix from YYYYMMDD to YYYY-MM-DD 2021-02-18 16:31:27 +11:00
meeb 0dc201b293 guide tweak 2021-02-18 16:26:58 +11:00
meeb 82fa0f6bce add sync.Source.download_media master flag, add manual import existing media command with docs, resolves #24 2021-02-18 16:24:24 +11:00
meeb 8b93cb4a59 handle malformed responses passively 2021-02-18 14:55:40 +11:00
meeb 647254d7f7 set X-Frame-Options to SAMEORIGIN by default, resolves #51 2021-02-17 22:25:13 +11:00
meeb 3567e20600 fix typing issue when videos have no votes and their vote count is None not 0 in metadata, resolves #50 2021-02-17 22:14:29 +11:00
meeb 5348e25303 update libs 2021-02-17 20:08:06 +11:00
meeb 749df3f7bb switch to using flat indexing of media, only index metadata for media once, resolves #38 and dramatically reduces crawl requests to youtube 2021-02-02 17:24:19 +11:00
meeb 2c2f53e5b2 update packages 2021-02-02 15:05:53 +11:00
meeb 06cfafb803 assert the healthcheck.py exec permissions are preserved, should resolve #41 2021-01-25 13:35:42 +11:00
meeb f5a37f2e86 update deps 2021-01-25 13:15:43 +11:00
meeb 36747a47e0
create FUNDING.yml 2021-01-22 15:40:26 +11:00
meeb ffd69e8d40 bump to v0.8 2021-01-20 18:13:28 +11:00
meeb eebef3371f fix form label padding from overlapping field, resolves #37 2021-01-20 18:03:24 +11:00
meeb 4cd6701c8a fix bug handling audio fallback detection, resolves #31 2021-01-20 18:00:28 +11:00
meeb 4ebe6f2a37 pin footer at the bottom of the viewport, resolves #26 2021-01-20 17:42:16 +11:00
meeb d553d58fde update youtube-dl 2021-01-20 17:34:50 +11:00
meeb df40a1367a sanitise youtube video titles for use in sane filenames, resolves #35 2021-01-20 17:34:19 +11:00
meeb 607ee77e70 update deps 2021-01-16 03:50:16 +11:00
meeb 9af493aa8a update upstream library versions 2021-01-03 15:18:31 +11:00
meeb f0c94ff789
Merge pull request #29 from ltomes/main
Added format descriptions.
2020-12-30 15:40:41 +11:00
Levi Tomes 39c7799831
Changed PR intent
Changed PR intent
2020-12-29 22:36:43 -06:00
Levi Tomes da7371f830
Added format descriptions
Added format descriptions to the media format help_text.
2020-12-29 21:50:04 -06:00
meeb 387cfefc8f issues with dupe background async worker threads, drop defaults back down to 1 worker 2020-12-21 03:04:54 +11:00
meeb d92dbde781 bump to 0.7 2020-12-20 18:24:04 +11:00
meeb e36658e1a1 missing default setting for container build 2020-12-20 17:57:04 +11:00
meeb 51cd942717 use async workers, spawn two workers by default, add TUBESYNC_WORKERS env var to adjust the number of workers, resolves #19 2020-12-20 17:52:45 +11:00
meeb 001554db1a fix format container name, resolves #22 2020-12-20 13:01:14 +11:00
meeb 7cf86bb98d fix attribute name, resolves #21 2020-12-20 04:15:43 +11:00
meeb c28c095f48 add :latest tag warning 2020-12-19 19:29:26 +11:00
meeb 12eac049e5 bump to 0.6 2020-12-19 18:14:31 +11:00
meeb 304cc153cf add DJANGO_FORCE_SCRIPT_NAME env var to change Djangos FORCE_SCRIPT_NAME if needed, part of support for running TubeSync in a /suburi and not a domain root, resolves #18 2020-12-19 18:10:10 +11:00
meeb b45231f533 add secondary time based cap to allow sources to not download everything in a channel, resolves #15 2020-12-19 18:05:01 +11:00
meeb 26eb9d30e8 tweak field help text 2020-12-19 18:04:26 +11:00
meeb 97fa62d12b add playlist_index and playlist_title as media format options, fix paths for files in media format subdirs post download, resolves #13 2020-12-19 17:33:08 +11:00
meeb 1b092fe955 use xml parsing for tests to fix annoying attr ordering 2020-12-19 16:31:44 +11:00
meeb 18a59fe835 use OrderedDict for XML attrs so testing is consistent 2020-12-19 16:09:19 +11:00
meeb 410906ad8e add XML NFO file writing support, rework media cleanup deletion, resolves #11 2020-12-19 16:00:37 +11:00
meeb 8f4b09f346 add {mm} and {dd} media format support, resolves #12 2020-12-18 21:02:06 +11:00
meeb cda021cbbf update screenshots 2020-12-18 19:01:35 +11:00
meeb ee4df99cd8 update screenshots 2020-12-18 18:57:52 +11:00
meeb 53f1873a9b Merge branch 'main' of github.com:meeb/tubesync into main 2020-12-18 18:41:36 +11:00
meeb 9434293a84 fix dupe info on dashboard 2020-12-18 18:41:24 +11:00
meeb ed69fe9dcc README tweaks 2020-12-18 18:35:58 +11:00
meeb 67af70569b bump to 0.5 2020-12-18 18:07:33 +11:00
meeb 68a62d8a7c add full support for YouTube channels with no vanity name, resolves #9 2020-12-18 17:43:58 +11:00
meeb 55578f4de7 add pretty-json-info-spam wrapper command to aid debugging urls 2020-12-18 17:31:47 +11:00
134 changed files with 15311 additions and 804 deletions

1
.github/FUNDING.yml vendored Normal file
View File

@ -0,0 +1 @@
github: [meeb]

View File

@ -4,12 +4,10 @@ env:
IMAGE_NAME: tubesync IMAGE_NAME: tubesync
on: on:
workflow_dispatch:
push: push:
branches: branches:
- main - main
pull_request:
branches:
- main
jobs: jobs:
test: test:
@ -27,7 +25,7 @@ jobs:
run: | run: |
python -m pip install --upgrade pip python -m pip install --upgrade pip
pip install pipenv pip install pipenv
pipenv install --system pipenv install --system --skip-lock
- name: Set up Django environment - name: Set up Django environment
run: cp tubesync/tubesync/local_settings.py.example tubesync/tubesync/local_settings.py run: cp tubesync/tubesync/local_settings.py.example tubesync/tubesync/local_settings.py
- name: Run Django tests - name: Run Django tests
@ -35,13 +33,24 @@ jobs:
containerise: containerise:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - name: Set up QEMU
- name: Build the container image uses: docker/setup-qemu-action@v1
run: docker build . --tag $IMAGE_NAME - name: Set up Docker Buildx
- name: Log into GitHub Container Registry uses: docker/setup-buildx-action@v1
run: echo "${{ secrets.REGISTRY_ACCESS_TOKEN }}" | docker login https://ghcr.io -u ${{ github.actor }} --password-stdin - name: Log into GitHub Container Registry
- name: Push image to GitHub Container Registry run: echo "${{ secrets.REGISTRY_ACCESS_TOKEN }}" | docker login https://ghcr.io -u ${{ github.actor }} --password-stdin
run: | - name: Lowercase github username for ghcr
LATEST_TAG=ghcr.io/meeb/$IMAGE_NAME:latest id: string
docker tag $IMAGE_NAME $LATEST_TAG uses: ASzc/change-string-case-action@v1
docker push $LATEST_TAG with:
string: ${{ github.actor }}
- name: Build and push
uses: docker/build-push-action@v2
with:
platforms: linux/amd64,linux/arm64
push: true
tags: ghcr.io/${{ steps.string.outputs.lowercase }}/${{ env.IMAGE_NAME }}:latest
cache-from: type=registry,ref=ghcr.io/${{ steps.string.outputs.lowercase }}/${{ env.IMAGE_NAME }}:latest
cache-to: type=inline
build-args: |
IMAGE_NAME=${{ env.IMAGE_NAME }}

View File

@ -11,18 +11,28 @@ jobs:
containerise: containerise:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - name: Set up QEMU
- name: Get tag uses: docker/setup-qemu-action@v1
id: vars - name: Get tag
run: echo ::set-output name=tag::${GITHUB_REF#refs/*/} id: tag
- name: Build the container image uses: dawidd6/action-get-tag@v1
run: docker build . --tag $IMAGE_NAME - uses: docker/build-push-action@v2
- name: Log into GitHub Container Registry - name: Set up Docker Buildx
run: echo "${{ secrets.REGISTRY_ACCESS_TOKEN }}" | docker login https://ghcr.io -u ${{ github.actor }} --password-stdin uses: docker/setup-buildx-action@v1
- name: Push image to GitHub Container Registry - name: Log into GitHub Container Registry
env: run: echo "${{ secrets.REGISTRY_ACCESS_TOKEN }}" | docker login https://ghcr.io -u ${{ github.actor }} --password-stdin
RELEASE_TAG: ${{ steps.vars.outputs.tag }} - name: Lowercase github username for ghcr
run: | id: string
REF_TAG=ghcr.io/meeb/$IMAGE_NAME:$RELEASE_TAG uses: ASzc/change-string-case-action@v1
docker tag $IMAGE_NAME $REF_TAG with:
docker push $REF_TAG string: ${{ github.actor }}
- name: Build and push
uses: docker/build-push-action@v2
with:
platforms: linux/amd64,linux/arm64
push: true
tags: ghcr.io/${{ steps.string.outputs.lowercase }}/${{ env.IMAGE_NAME }}:${{ steps.tag.outputs.tag }}
cache-from: type=registry,ref=ghcr.io/${{ steps.string.outputs.lowercase }}/${{ env.IMAGE_NAME }}:${{ steps.tag.outputs.tag }}
cache-to: type=inline
build-args: |
IMAGE_NAME=${{ env.IMAGE_NAME }}

4
.gitignore vendored
View File

@ -1,3 +1,4 @@
.DS_Store
# Byte-compiled / optimized / DLL files # Byte-compiled / optimized / DLL files
__pycache__/ __pycache__/
*.py[cod] *.py[cod]
@ -130,3 +131,6 @@ dmypy.json
# Pyre type checker # Pyre type checker
.pyre/ .pyre/
Pipfile.lock
.vscode/launch.json

View File

@ -1,72 +1,112 @@
FROM debian:buster-slim FROM debian:bookworm-slim
ARG ARCH="amd64" ARG TARGETPLATFORM
ARG S6_VERSION="2.1.0.2" ARG S6_VERSION="3.1.5.0"
ARG FFMPEG_VERSION="4.3.1" ARG FFMPEG_DATE="autobuild-2023-11-29-14-19"
ARG FFMPEG_VERSION="112875-g47e214245b"
ENV DEBIAN_FRONTEND="noninteractive" \ ENV DEBIAN_FRONTEND="noninteractive" \
HOME="/root" \ HOME="/root" \
LANGUAGE="en_US.UTF-8" \ LANGUAGE="en_US.UTF-8" \
LANG="en_US.UTF-8" \ LANG="en_US.UTF-8" \
LC_ALL="en_US.UTF-8" \ LC_ALL="en_US.UTF-8" \
TERM="xterm" \ TERM="xterm" \
S6_EXPECTED_SHA256="52460473413601ff7a84ae690b161a074217ddc734990c2cdee9847166cf669e" \ S6_CMD_WAIT_FOR_SERVICES_MAXTIME="0"
S6_DOWNLOAD="https://github.com/just-containers/s6-overlay/releases/download/v${S6_VERSION}/s6-overlay-${ARCH}.tar.gz" \
FFMPEG_EXPECTED_SHA256="47d95c0129fba27d051748a442a44a73ce1bd38d1e3f9fe1e9dd7258c7581fa5" \
FFMPEG_DOWNLOAD="https://tubesync.sfo2.digitaloceanspaces.com/ffmpeg-${FFMPEG_VERSION}-${ARCH}-static.tar.xz"
# Install third party software # Install third party software
RUN set -x && \ RUN export ARCH=$(case ${TARGETPLATFORM:-linux/amd64} in \
apt-get update && \ "linux/amd64") echo "amd64" ;; \
apt-get -y --no-install-recommends install locales && \ "linux/arm64") echo "aarch64" ;; \
echo "en_US.UTF-8 UTF-8" > /etc/locale.gen && \ *) echo "" ;; esac) && \
locale-gen en_US.UTF-8 && \ export S6_ARCH_EXPECTED_SHA256=$(case ${TARGETPLATFORM:-linux/amd64} in \
# Install required distro packages "linux/amd64") echo "65d0d0f353d2ff9d0af202b268b4bf53a9948a5007650854855c729289085739" ;; \
apt-get -y --no-install-recommends install curl xz-utils ca-certificates binutils && \ "linux/arm64") echo "3fbd14201473710a592b2189e81f00f3c8998e96d34f16bd2429c35d1bc36d00" ;; \
# Install s6 *) echo "" ;; esac) && \
curl -L ${S6_DOWNLOAD} --output /tmp/s6-overlay-${ARCH}.tar.gz && \ export S6_DOWNLOAD_ARCH=$(case ${TARGETPLATFORM:-linux/amd64} in \
sha256sum /tmp/s6-overlay-${ARCH}.tar.gz && \ "linux/amd64") echo "https://github.com/just-containers/s6-overlay/releases/download/v${S6_VERSION}/s6-overlay-x86_64.tar.xz" ;; \
echo "${S6_EXPECTED_SHA256} /tmp/s6-overlay-${ARCH}.tar.gz" | sha256sum -c - && \ "linux/arm64") echo "https://github.com/just-containers/s6-overlay/releases/download/v${S6_VERSION}/s6-overlay-aarch64.tar.xz" ;; \
tar xzf /tmp/s6-overlay-${ARCH}.tar.gz -C / && \ *) echo "" ;; esac) && \
# Install ffmpeg export FFMPEG_EXPECTED_SHA256=$(case ${TARGETPLATFORM:-linux/amd64} in \
curl -L ${FFMPEG_DOWNLOAD} --output /tmp/ffmpeg-${ARCH}-static.tar.xz && \ "linux/amd64") echo "36bac8c527bf390603416f749ab0dd860142b0a66f0865b67366062a9c286c8b" ;; \
echo "${FFMPEG_EXPECTED_SHA256} /tmp/ffmpeg-${ARCH}-static.tar.xz" | sha256sum -c - && \ "linux/arm64") echo "8f36e45d99d2367a5c0c220ee3164fa48f4f0cec35f78204ccced8dc303bfbdc" ;; \
xz --decompress /tmp/ffmpeg-${ARCH}-static.tar.xz && \ *) echo "" ;; esac) && \
tar -xvf /tmp/ffmpeg-${ARCH}-static.tar -C /tmp && \ export FFMPEG_DOWNLOAD=$(case ${TARGETPLATFORM:-linux/amd64} in \
install -v -s -g root -o root -m 0755 -s /tmp/ffmpeg-${FFMPEG_VERSION}-${ARCH}-static/ffmpeg -t /usr/local/bin && \ "linux/amd64") echo "https://github.com/yt-dlp/FFmpeg-Builds/releases/download/${FFMPEG_DATE}/ffmpeg-N-${FFMPEG_VERSION}-linux64-gpl.tar.xz" ;; \
# Clean up "linux/arm64") echo "https://github.com/yt-dlp/FFmpeg-Builds/releases/download/${FFMPEG_DATE}/ffmpeg-N-${FFMPEG_VERSION}-linuxarm64-gpl.tar.xz" ;; \
rm -rf /tmp/s6-overlay-${ARCH}.tar.gz && \ *) echo "" ;; esac) && \
rm -rf /tmp/ffmpeg-${ARCH}-static.tar && \ export S6_NOARCH_EXPECTED_SHA256="fd80c231e8ae1a0667b7ae2078b9ad0e1269c4d117bf447a4506815a700dbff3" && \
rm -rf /tmp/ffmpeg-${FFMPEG_VERSION}-${ARCH}-static && \ export S6_DOWNLOAD_NOARCH="https://github.com/just-containers/s6-overlay/releases/download/v${S6_VERSION}/s6-overlay-noarch.tar.xz" && \
apt-get -y autoremove --purge curl xz-utils binutils echo "Building for arch: ${ARCH}|${ARCH44}, downloading S6 from: ${S6_DOWNLOAD}}, expecting S6 SHA256: ${S6_EXPECTED_SHA256}" && \
set -x && \
apt-get update && \
apt-get -y --no-install-recommends install locales && \
echo "en_US.UTF-8 UTF-8" > /etc/locale.gen && \
locale-gen en_US.UTF-8 && \
# Install required distro packages
apt-get -y --no-install-recommends install curl ca-certificates binutils xz-utils && \
# Install s6
curl -L ${S6_DOWNLOAD_NOARCH} --output /tmp/s6-overlay-noarch.tar.xz && \
echo "${S6_NOARCH_EXPECTED_SHA256} /tmp/s6-overlay-noarch.tar.xz" | sha256sum -c - && \
tar -C / -Jxpf /tmp/s6-overlay-noarch.tar.xz && \
curl -L ${S6_DOWNLOAD_ARCH} --output /tmp/s6-overlay-${ARCH}.tar.xz && \
echo "${S6_ARCH_EXPECTED_SHA256} /tmp/s6-overlay-${ARCH}.tar.xz" | sha256sum -c - && \
tar -C / -Jxpf /tmp/s6-overlay-${ARCH}.tar.xz && \
# Install ffmpeg
echo "Building for arch: ${ARCH}|${ARCH44}, downloading FFMPEG from: ${FFMPEG_DOWNLOAD}, expecting FFMPEG SHA256: ${FFMPEG_EXPECTED_SHA256}" && \
curl -L ${FFMPEG_DOWNLOAD} --output /tmp/ffmpeg-${ARCH}.tar.xz && \
sha256sum /tmp/ffmpeg-${ARCH}.tar.xz && \
echo "${FFMPEG_EXPECTED_SHA256} /tmp/ffmpeg-${ARCH}.tar.xz" | sha256sum -c - && \
tar -xf /tmp/ffmpeg-${ARCH}.tar.xz --strip-components=2 --no-anchored -C /usr/local/bin/ "ffmpeg" && \
tar -xf /tmp/ffmpeg-${ARCH}.tar.xz --strip-components=2 --no-anchored -C /usr/local/bin/ "ffprobe" && \
# Clean up
rm -rf /tmp/s6-overlay-${ARCH}.tar.gz && \
rm -rf /tmp/ffmpeg-${ARCH}.tar.xz && \
apt-get -y autoremove --purge curl binutils xz-utils
# Copy app # Copy app
COPY tubesync /app COPY tubesync /app
COPY tubesync/tubesync/local_settings.py.container /app/tubesync/local_settings.py COPY tubesync/tubesync/local_settings.py.container /app/tubesync/local_settings.py
# Append container bundled software versions # Copy over pip.conf to use piwheels
RUN echo "ffmpeg_version = '${FFMPEG_VERSION}-static'" >> /app/common/third_party_versions.py COPY pip.conf /etc/pip.conf
# Add Pipfile # Add Pipfile
COPY Pipfile /app/Pipfile COPY Pipfile /app/Pipfile
COPY Pipfile.lock /app/Pipfile.lock
# Switch workdir to the the app # Switch workdir to the the app
WORKDIR /app WORKDIR /app
# Set up the app # Set up the app
RUN set -x && \ RUN set -x && \
apt-get update && \
# Install required distro packages # Install required distro packages
apt-get -y install nginx-light && \ apt-get -y install nginx-light && \
apt-get -y --no-install-recommends install python3 python3-setuptools python3-pip python3-dev gcc make && \ apt-get -y --no-install-recommends install \
# Install pipenv python3 \
pip3 --disable-pip-version-check install pipenv && \ python3-dev \
python3-pip \
python3-wheel \
pipenv \
gcc \
g++ \
make \
pkgconf \
default-libmysqlclient-dev \
libmariadb3 \
postgresql-common \
libpq-dev \
libpq5 \
libjpeg62-turbo \
libwebp7 \
libjpeg-dev \
zlib1g-dev \
libwebp-dev \
redis-server && \
# Create a 'app' user which the application will run as # Create a 'app' user which the application will run as
groupadd app && \ groupadd app && \
useradd -M -d /app -s /bin/false -g app app && \ useradd -M -d /app -s /bin/false -g app app && \
# Install non-distro packages # Install non-distro packages
pipenv install --system && \ PIPENV_VERBOSITY=64 pipenv install --system --skip-lock && \
# Make absolutely sure we didn't accidentally bundle a SQLite dev database # Make absolutely sure we didn't accidentally bundle a SQLite dev database
rm -rf /app/db.sqlite3 && \ rm -rf /app/db.sqlite3 && \
# Run any required app commands # Run any required app commands
@ -79,10 +119,19 @@ RUN set -x && \
mkdir -p /downloads/video && \ mkdir -p /downloads/video && \
# Clean up # Clean up
rm /app/Pipfile && \ rm /app/Pipfile && \
rm /app/Pipfile.lock && \
pipenv --clear && \ pipenv --clear && \
pip3 --disable-pip-version-check uninstall -y pipenv wheel virtualenv && \ apt-get -y autoremove --purge \
apt-get -y autoremove --purge python3-pip python3-dev gcc make && \ python3-pip \
python3-dev \
gcc \
g++ \
make \
default-libmysqlclient-dev \
postgresql-common \
libpq-dev \
libjpeg-dev \
zlib1g-dev \
libwebp-dev && \
apt-get -y autoremove && \ apt-get -y autoremove && \
apt-get -y autoclean && \ apt-get -y autoclean && \
rm -rf /var/lib/apt/lists/* && \ rm -rf /var/lib/apt/lists/* && \
@ -92,7 +141,12 @@ RUN set -x && \
rm -rf /root && \ rm -rf /root && \
mkdir -p /root && \ mkdir -p /root && \
chown root:root /root && \ chown root:root /root && \
chmod 0700 /root chmod 0755 /root
# Append software versions
RUN set -x && \
FFMPEG_VERSION=$(/usr/local/bin/ffmpeg -version | head -n 1 | awk '{ print $3 }') && \
echo "ffmpeg_version = '${FFMPEG_VERSION}'" >> /app/common/third_party_versions.py
# Copy root # Copy root
COPY config/root / COPY config/root /
@ -102,7 +156,7 @@ HEALTHCHECK --interval=1m --timeout=10s CMD /app/healthcheck.py http://127.0.0.1
# ENVS and ports # ENVS and ports
ENV PYTHONPATH "/app:${PYTHONPATH}" ENV PYTHONPATH "/app:${PYTHONPATH}"
EXPOSE 8080 EXPOSE 4848
# Volumes # Volumes
VOLUME ["/config", "/downloads"] VOLUME ["/config", "/downloads"]

View File

@ -8,17 +8,17 @@ all: clean build
dev: dev:
$(python) app/manage.py runserver $(python) tubesync/manage.py runserver
build: build:
mkdir -p app/media mkdir -p tubesync/media
mkdir -p app/static mkdir -p tubesync/static
$(python) app/manage.py collectstatic --noinput $(python) tubesync/manage.py collectstatic --noinput
clean: clean:
rm -rf app/static rm -rf tubesync/static
container: clean container: clean
@ -29,5 +29,13 @@ runcontainer:
$(docker) run --rm --name $(name) --env-file dev.env --log-opt max-size=50m -ti -p 4848:4848 $(image) $(docker) run --rm --name $(name) --env-file dev.env --log-opt max-size=50m -ti -p 4848:4848 $(image)
test: stopcontainer:
$(python) app/manage.py test --verbosity=2 $(docker) stop $(name)
test: build
cd tubesync && $(python) manage.py test --verbosity=2 && cd ..
shell:
cd tubesync && $(python) manage.py shell

15
Pipfile
View File

@ -4,9 +4,10 @@ url = "https://pypi.org/simple"
verify_ssl = true verify_ssl = true
[dev-packages] [dev-packages]
autopep8 = "*"
[packages] [packages]
django = "*" django = "~=3.2"
django-sass-processor = "*" django-sass-processor = "*"
libsass = "*" libsass = "*"
pillow = "*" pillow = "*"
@ -14,9 +15,11 @@ whitenoise = "*"
gunicorn = "*" gunicorn = "*"
django-compressor = "*" django-compressor = "*"
httptools = "*" httptools = "*"
youtube-dl = "*"
django-background-tasks = "*" django-background-tasks = "*"
requests = "*" django-basicauth = "*"
psycopg2-binary = "*"
[requires] mysqlclient = "*"
python_version = "3" yt-dlp = "*"
redis = "*"
hiredis = "*"
requests = {extras = ["socks"], version = "*"}

247
Pipfile.lock generated
View File

@ -1,247 +0,0 @@
{
"_meta": {
"hash": {
"sha256": "a4bb556fc61ee4583f9588980450b071814298ee4d1a1023fad149c14d14aaba"
},
"pipfile-spec": 6,
"requires": {
"python_version": "3"
},
"sources": [
{
"name": "pypi",
"url": "https://pypi.org/simple",
"verify_ssl": true
}
]
},
"default": {
"asgiref": {
"hashes": [
"sha256:5ee950735509d04eb673bd7f7120f8fa1c9e2df495394992c73234d526907e17",
"sha256:7162a3cb30ab0609f1a4c95938fd73e8604f63bdba516a7f7d64b83ff09478f0"
],
"version": "==3.3.1"
},
"certifi": {
"hashes": [
"sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c",
"sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830"
],
"version": "==2020.12.5"
},
"chardet": {
"hashes": [
"sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa",
"sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"
],
"version": "==4.0.0"
},
"django": {
"hashes": [
"sha256:5c866205f15e7a7123f1eec6ab939d22d5bde1416635cab259684af66d8e48a2",
"sha256:edb10b5c45e7e9c0fb1dc00b76ec7449aca258a39ffd613dbd078c51d19c9f03"
],
"index": "pypi",
"version": "==3.1.4"
},
"django-appconf": {
"hashes": [
"sha256:1b1d0e1069c843ebe8ae5aa48ec52403b1440402b320c3e3a206a0907e97bb06",
"sha256:be58deb54a43d77d2e1621fe59f787681376d3cd0b8bd8e4758ef6c3a6453380"
],
"version": "==1.0.4"
},
"django-background-tasks": {
"hashes": [
"sha256:e1b19e8d495a276c9d64c5a1ff8b41132f75d2f58e45be71b78650dad59af9de"
],
"index": "pypi",
"version": "==1.2.5"
},
"django-compat": {
"hashes": [
"sha256:3ac9a3bedc56b9365d9eb241bc5157d0c193769bf995f9a78dc1bc24e7c2331b"
],
"version": "==1.0.15"
},
"django-compressor": {
"hashes": [
"sha256:57ac0a696d061e5fc6fbc55381d2050f353b973fb97eee5593f39247bc0f30af",
"sha256:d2ed1c6137ddaac5536233ec0a819e14009553fee0a869bea65d03e5285ba74f"
],
"index": "pypi",
"version": "==2.4"
},
"django-sass-processor": {
"hashes": [
"sha256:9b46a12ca8bdcb397d46fbcc49e6a926ff9f76a93c5efeb23b495419fd01fc7a"
],
"index": "pypi",
"version": "==0.8.2"
},
"gunicorn": {
"hashes": [
"sha256:1904bb2b8a43658807108d59c3f3d56c2b6121a701161de0ddf9ad140073c626",
"sha256:cd4a810dd51bf497552cf3f863b575dabd73d6ad6a91075b65936b151cbf4f9c"
],
"index": "pypi",
"version": "==20.0.4"
},
"httptools": {
"hashes": [
"sha256:0a4b1b2012b28e68306575ad14ad5e9120b34fccd02a81eb08838d7e3bbb48be",
"sha256:3592e854424ec94bd17dc3e0c96a64e459ec4147e6d53c0a42d0ebcef9cb9c5d",
"sha256:41b573cf33f64a8f8f3400d0a7faf48e1888582b6f6e02b82b9bd4f0bf7497ce",
"sha256:56b6393c6ac7abe632f2294da53f30d279130a92e8ae39d8d14ee2e1b05ad1f2",
"sha256:86c6acd66765a934e8730bf0e9dfaac6fdcf2a4334212bd4a0a1c78f16475ca6",
"sha256:96da81e1992be8ac2fd5597bf0283d832287e20cb3cfde8996d2b00356d4e17f",
"sha256:96eb359252aeed57ea5c7b3d79839aaa0382c9d3149f7d24dd7172b1bcecb009",
"sha256:a2719e1d7a84bb131c4f1e0cb79705034b48de6ae486eb5297a139d6a3296dce",
"sha256:ac0aa11e99454b6a66989aa2d44bca41d4e0f968e395a0a8f164b401fefe359a",
"sha256:bc3114b9edbca5a1eb7ae7db698c669eb53eb8afbbebdde116c174925260849c",
"sha256:fa3cd71e31436911a44620473e873a256851e1f53dee56669dae403ba41756a4",
"sha256:fea04e126014169384dee76a153d4573d90d0cbd1d12185da089f73c78390437"
],
"index": "pypi",
"version": "==0.1.1"
},
"idna": {
"hashes": [
"sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6",
"sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"
],
"version": "==2.10"
},
"libsass": {
"hashes": [
"sha256:1521d2a8d4b397c6ec90640a1f6b5529077035efc48ef1c2e53095544e713d1b",
"sha256:1b2d415bbf6fa7da33ef46e549db1418498267b459978eff8357e5e823962d35",
"sha256:25ebc2085f5eee574761ccc8d9cd29a9b436fc970546d5ef08c6fa41eb57dff1",
"sha256:2ae806427b28bc1bb7cb0258666d854fcf92ba52a04656b0b17ba5e190fb48a9",
"sha256:4a246e4b88fd279abef8b669206228c92534d96ddcd0770d7012088c408dff23",
"sha256:553e5096414a8d4fb48d0a48f5a038d3411abe254d79deac5e008516c019e63a",
"sha256:697f0f9fa8a1367ca9ec6869437cb235b1c537fc8519983d1d890178614a8903",
"sha256:a8fd4af9f853e8bf42b1425c5e48dd90b504fa2e70d7dac5ac80b8c0a5a5fe85",
"sha256:c9411fec76f480ffbacc97d8188322e02a5abca6fc78e70b86a2a2b421eae8a2",
"sha256:daa98a51086d92aa7e9c8871cf1a8258124b90e2abf4697852a3dca619838618",
"sha256:e0e60836eccbf2d9e24ec978a805cd6642fa92515fbd95e3493fee276af76f8a",
"sha256:e64ae2587f1a683e831409aad03ba547c245ef997e1329fffadf7a866d2510b8",
"sha256:f6852828e9e104d2ce0358b73c550d26dd86cc3a69439438c3b618811b9584f5"
],
"index": "pypi",
"version": "==0.20.1"
},
"pillow": {
"hashes": [
"sha256:006de60d7580d81f4a1a7e9f0173dc90a932e3905cc4d47ea909bc946302311a",
"sha256:0a2e8d03787ec7ad71dc18aec9367c946ef8ef50e1e78c71f743bc3a770f9fae",
"sha256:0eeeae397e5a79dc088d8297a4c2c6f901f8fb30db47795113a4a605d0f1e5ce",
"sha256:11c5c6e9b02c9dac08af04f093eb5a2f84857df70a7d4a6a6ad461aca803fb9e",
"sha256:2fb113757a369a6cdb189f8df3226e995acfed0a8919a72416626af1a0a71140",
"sha256:4b0ef2470c4979e345e4e0cc1bbac65fda11d0d7b789dbac035e4c6ce3f98adb",
"sha256:59e903ca800c8cfd1ebe482349ec7c35687b95e98cefae213e271c8c7fffa021",
"sha256:5abd653a23c35d980b332bc0431d39663b1709d64142e3652890df4c9b6970f6",
"sha256:5f9403af9c790cc18411ea398a6950ee2def2a830ad0cfe6dc9122e6d528b302",
"sha256:6b4a8fd632b4ebee28282a9fef4c341835a1aa8671e2770b6f89adc8e8c2703c",
"sha256:6c1aca8231625115104a06e4389fcd9ec88f0c9befbabd80dc206c35561be271",
"sha256:795e91a60f291e75de2e20e6bdd67770f793c8605b553cb6e4387ce0cb302e09",
"sha256:7ba0ba61252ab23052e642abdb17fd08fdcfdbbf3b74c969a30c58ac1ade7cd3",
"sha256:7c9401e68730d6c4245b8e361d3d13e1035cbc94db86b49dc7da8bec235d0015",
"sha256:81f812d8f5e8a09b246515fac141e9d10113229bc33ea073fec11403b016bcf3",
"sha256:895d54c0ddc78a478c80f9c438579ac15f3e27bf442c2a9aa74d41d0e4d12544",
"sha256:8de332053707c80963b589b22f8e0229f1be1f3ca862a932c1bcd48dafb18dd8",
"sha256:92c882b70a40c79de9f5294dc99390671e07fc0b0113d472cbea3fde15db1792",
"sha256:95edb1ed513e68bddc2aee3de66ceaf743590bf16c023fb9977adc4be15bd3f0",
"sha256:b63d4ff734263ae4ce6593798bcfee6dbfb00523c82753a3a03cbc05555a9cc3",
"sha256:bd7bf289e05470b1bc74889d1466d9ad4a56d201f24397557b6f65c24a6844b8",
"sha256:cc3ea6b23954da84dbee8025c616040d9aa5eaf34ea6895a0a762ee9d3e12e11",
"sha256:cc9ec588c6ef3a1325fa032ec14d97b7309db493782ea8c304666fb10c3bd9a7",
"sha256:d3d07c86d4efa1facdf32aa878bd508c0dc4f87c48125cc16b937baa4e5b5e11",
"sha256:d8a96747df78cda35980905bf26e72960cba6d355ace4780d4bdde3b217cdf1e",
"sha256:e38d58d9138ef972fceb7aeec4be02e3f01d383723965bfcef14d174c8ccd039",
"sha256:eb472586374dc66b31e36e14720747595c2b265ae962987261f044e5cce644b5",
"sha256:fbd922f702582cb0d71ef94442bfca57624352622d75e3be7a1e7e9360b07e72"
],
"index": "pypi",
"version": "==8.0.1"
},
"pytz": {
"hashes": [
"sha256:3e6b7dd2d1e0a59084bcee14a17af60c5c562cdc16d828e8eba2e683d3a7e268",
"sha256:5c55e189b682d420be27c6995ba6edce0c0a77dd67bfbe2ae6607134d5851ffd"
],
"version": "==2020.4"
},
"rcssmin": {
"hashes": [
"sha256:ca87b695d3d7864157773a61263e5abb96006e9ff0e021eff90cbe0e1ba18270"
],
"version": "==1.0.6"
},
"requests": {
"hashes": [
"sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804",
"sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"
],
"index": "pypi",
"version": "==2.25.1"
},
"rjsmin": {
"hashes": [
"sha256:0ab825839125eaca57cc59581d72e596e58a7a56fbc0839996b7528f0343a0a8",
"sha256:211c2fe8298951663bbc02acdffbf714f6793df54bfc50e1c6c9e71b3f2559a3",
"sha256:466fe70cc5647c7c51b3260c7e2e323a98b2b173564247f9c89e977720a0645f",
"sha256:585e75a84d9199b68056fd4a083d9a61e2a92dfd10ff6d4ce5bdb04bc3bdbfaf",
"sha256:6044ca86e917cd5bb2f95e6679a4192cef812122f28ee08c677513de019629b3",
"sha256:714329db774a90947e0e2086cdddb80d5e8c4ac1c70c9f92436378dedb8ae345",
"sha256:799890bd07a048892d8d3deb9042dbc20b7f5d0eb7da91e9483c561033b23ce2",
"sha256:975b69754d6a76be47c0bead12367a1ca9220d08e5393f80bab0230d4625d1f4",
"sha256:b15dc75c71f65d9493a8c7fa233fdcec823e3f1b88ad84a843ffef49b338ac32",
"sha256:dd0f4819df4243ffe4c964995794c79ca43943b5b756de84be92b445a652fb86",
"sha256:e3908b21ebb584ce74a6ac233bdb5f29485752c9d3be5e50c5484ed74169232c",
"sha256:e487a7783ac4339e79ec610b98228eb9ac72178973e3dee16eba0e3feef25924",
"sha256:ecd29f1b3e66a4c0753105baec262b331bcbceefc22fbe6f7e8bcd2067bcb4d7"
],
"version": "==1.1.0"
},
"six": {
"hashes": [
"sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259",
"sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"
],
"version": "==1.15.0"
},
"sqlparse": {
"hashes": [
"sha256:017cde379adbd6a1f15a61873f43e8274179378e95ef3fede90b5aa64d304ed0",
"sha256:0f91fd2e829c44362cbcfab3e9ae12e22badaa8a29ad5ff599f9ec109f0454e8"
],
"version": "==0.4.1"
},
"urllib3": {
"hashes": [
"sha256:19188f96923873c92ccb987120ec4acaa12f0461fa9ce5d3d0772bc965a39e08",
"sha256:d8ff90d979214d7b4f8ce956e80f4028fc6860e4431f731ea4a8c08f23f99473"
],
"version": "==1.26.2"
},
"whitenoise": {
"hashes": [
"sha256:05ce0be39ad85740a78750c86a93485c40f08ad8c62a6006de0233765996e5c7",
"sha256:05d00198c777028d72d8b0bbd234db605ef6d60e9410125124002518a48e515d"
],
"index": "pypi",
"version": "==5.2.0"
},
"youtube-dl": {
"hashes": [
"sha256:65968065e66966955dc79fad9251565fcc982566118756da624bd21467f3a04c",
"sha256:eaa859f15b6897bec21474b7787dc958118c8088e1f24d4ef1d58eab13188958"
],
"index": "pypi",
"version": "==2020.12.14"
}
},
"develop": {}
}

158
README.md
View File

@ -9,42 +9,43 @@ downloaded.
If you want to watch YouTube videos in particular quality or settings from your local If you want to watch YouTube videos in particular quality or settings from your local
media server, then TubeSync is for you. Internally, TubeSync is a web interface wrapper media server, then TubeSync is for you. Internally, TubeSync is a web interface wrapper
on `youtube-dl` and `ffmpeg` with a task scheduler. on `yt-dlp` and `ffmpeg` with a task scheduler.
There are several other web interfaces to YouTube and `youtube-dl` all with varying There are several other web interfaces to YouTube and `yt-dlp` all with varying
features and implemenations. TubeSync's largest difference is full PVR experience of features and implementations. TubeSync's largest difference is full PVR experience of
updating media servers and better selection of media formats. Additionally, to be as updating media servers and better selection of media formats. Additionally, to be as
hands-free as possible, TubeSync has gradual retrying of failures with back-off timers hands-free as possible, TubeSync has gradual retrying of failures with back-off timers
so media which fails to download will be retried for an extended period making it, so media which fails to download will be retried for an extended period making it,
hopefully, quite reliable. hopefully, quite reliable.
# Latest container image # Latest container image
```yaml ```yaml
ghcr.io/meeb/tubesync:v0.4 ghcr.io/meeb/tubesync:latest
``` ```
# Screenshots # Screenshots
### Dashboard ### Dashboard
![TubeSync Dashboard](https://github.com/meeb/tubesync/blob/main/docs/dashboard.png?raw=true) ![TubeSync Dashboard](https://github.com/meeb/tubesync/blob/main/docs/dashboard-v0.5.png?raw=true)
### Sources overview ### Sources overview
![TubeSync sources overview](https://github.com/meeb/tubesync/blob/main/docs/sources.png?raw=true) ![TubeSync sources overview](https://github.com/meeb/tubesync/blob/main/docs/sources-v0.5.png?raw=true)
### Source details ### Source details
![TubeSync source details](https://github.com/meeb/tubesync/blob/main/docs/source.png?raw=true) ![TubeSync source details](https://github.com/meeb/tubesync/blob/main/docs/source-v0.5.png?raw=true)
### Media overview ### Media overview
![TubeSync media overview](https://github.com/meeb/tubesync/blob/main/docs/media.png?raw=true) ![TubeSync media overview](https://github.com/meeb/tubesync/blob/main/docs/media-v0.5.png?raw=true)
### Media details ### Media details
![TubeSync media-details](https://github.com/meeb/tubesync/blob/main/docs/media-item.png?raw=true) ![TubeSync media-details](https://github.com/meeb/tubesync/blob/main/docs/media-item-v0.5.png?raw=true)
# Requirements # Requirements
@ -68,11 +69,12 @@ currently just Plex, to complete the PVR experience.
# Installation # Installation
TubeSync is designed to be run in a container, such as via Docker or Podman. It also TubeSync is designed to be run in a container, such as via Docker or Podman. It also
works in a Docker Compose stack. Only `amd64` is initially supported. works in a Docker Compose stack. `amd64` (most desktop PCs and servers) and `arm64`
(modern ARM computers, such as the Rasperry Pi 3 or later) are supported.
Example (with Docker on *nix): Example (with Docker on *nix):
First find your the user ID and group ID you want to run TubeSync as, if you're not First find the user ID and group ID you want to run TubeSync as, if you're not
sure what this is it's probably your current user ID and group ID: sure what this is it's probably your current user ID and group ID:
```bash ```bash
@ -97,8 +99,8 @@ $ mkdir /some/directory/tubesync-downloads
Finally, download and run the container: Finally, download and run the container:
```bash ```bash
# Pull a versioned image # Pull image
$ docker pull ghcr.io/meeb/tubesync:v0.4 $ docker pull ghcr.io/meeb/tubesync:latest
# Start the container using your user ID and group ID # Start the container using your user ID and group ID
$ docker run \ $ docker run \
-d \ -d \
@ -109,19 +111,21 @@ $ docker run \
-v /some/directory/tubesync-config:/config \ -v /some/directory/tubesync-config:/config \
-v /some/directory/tubesync-downloads:/downloads \ -v /some/directory/tubesync-downloads:/downloads \
-p 4848:4848 \ -p 4848:4848 \
ghcr.io/meeb/tubesync:v0.4 ghcr.io/meeb/tubesync:latest
``` ```
Once running, open `http://localhost:4848` in your browser and you should see the Once running, open `http://localhost:4848` in your browser and you should see the
TubeSync dashboard. If you do, you can proceed to adding some sources (YouTube channels TubeSync dashboard. If you do, you can proceed to adding some sources (YouTube channels
and playlists). If not, check `docker logs tubesync` to see what errors might be and playlists). If not, check `docker logs tubesync` to see what errors might be
occuring, typical ones are file permission issues. occurring, typical ones are file permission issues.
Alternatively, for Docker Compose, you can use something like: Alternatively, for Docker Compose, you can use something like:
```yaml ```yml
version: '3.7'
services:
tubesync: tubesync:
image: ghcr.io/meeb/tubesync:v0.4 image: ghcr.io/meeb/tubesync:latest
container_name: tubesync container_name: tubesync
restart: unless-stopped restart: unless-stopped
ports: ports:
@ -135,6 +139,41 @@ Alternatively, for Docker Compose, you can use something like:
- PGID=1000 - PGID=1000
``` ```
## Optional authentication
Available in `v1.0` (or `:latest`)and later. If you want to enable a basic username and
password to be required to access the TubeSync dashboard you can set them with the
following environment variables:
```bash
HTTP_USER
HTTP_PASS
```
For example, in the `docker run ...` line add in:
```bash
...
-e HTTP_USER=some-username \
-e HTTP_PASS=some-secure-password \
...
```
Or in your Docker Compose file you would add in:
```yaml
...
environment:
- HTTP_USER=some-username
- HTTP_PASS=some-secure-password
...
```
When BOTH `HTTP_USER` and `HTTP_PASS` are set then basic HTTP authentication will be
enabled.
# Updating # Updating
To update, you can just pull a new version of the container image as they are released. To update, you can just pull a new version of the container image as they are released.
@ -192,14 +231,27 @@ $ docker logs --follow tubesync
``` ```
# Advanced usage guides
Once you're happy using TubeSync there are some advanced usage guides for more complex
and less common features:
* [Import existing media into TubeSync](https://github.com/meeb/tubesync/blob/main/docs/import-existing-media.md)
* [Sync or create missing metadata files](https://github.com/meeb/tubesync/blob/main/docs/create-missing-metadata.md)
* [Reset tasks from the command line](https://github.com/meeb/tubesync/blob/main/docs/reset-tasks.md)
* [Using PostgreSQL, MySQL or MariaDB as database backends](https://github.com/meeb/tubesync/blob/main/docs/other-database-backends.md)
* [Using cookies](https://github.com/meeb/tubesync/blob/main/docs/using-cookies.md)
* [Reset metadata](https://github.com/meeb/tubesync/blob/main/docs/reset-metadata.md)
# Warnings # Warnings
### 1. Index frequency ### 1. Index frequency
It's a good idea to add sources with as low an index frequency as possible. This is the It's a good idea to add sources with as long of an index frequency as possible. This is
duration between indexes of the source. An index is when TubeSync checks to see the duration between indexes of the source. An index is when TubeSync checks to see
what videos available on a channel or playlist to find new media. Try and keep this as what videos available on a channel or playlist to find new media. Try and keep this as
long as possible, 24 hours if possible. long as possible, up to 24 hours.
### 2. Indexing massive channels ### 2. Indexing massive channels
@ -209,6 +261,14 @@ every hour" or similar short interval it's entirely possible your TubeSync insta
spend its entire time just indexing the massive channel over and over again without spend its entire time just indexing the massive channel over and over again without
downloading any media. Check your tasks for the status of your TubeSync install. downloading any media. Check your tasks for the status of your TubeSync install.
If you add a significant amount of "work" due to adding many large channels you may
need to increase the number of background workers by setting the `TUBESYNC_WORKERS`
environment variable. Try around ~4 at most, although the absolute maximum allowed is 8.
**Be nice.** it's likely entirely possible your IP address could get throttled by the
source if you try and crawl extremely large amounts very quickly. **Try and be polite
with the smallest amount of indexing and concurrent downloads possible for your needs.**
# FAQ # FAQ
@ -222,8 +282,8 @@ automatically.
### Does TubeSync support any other video platforms? ### Does TubeSync support any other video platforms?
At the moment, no. This is a first release. The library TubeSync uses that does most At the moment, no. This is a pre-release. The library TubeSync uses that does most
of the downloading work, `youtube-dl`, supports many hundreds of video sources so it's of the downloading work, `yt-dlp`, supports many hundreds of video sources so it's
likely more will be added to TubeSync if there is demand for it. likely more will be added to TubeSync if there is demand for it.
### Is there a progress bar? ### Is there a progress bar?
@ -235,27 +295,27 @@ your install is doing check the container logs.
### Are there alerts when a download is complete? ### Are there alerts when a download is complete?
No, this feature is best served by existing services such as the execelent No, this feature is best served by existing services such as the excellent
[tautulli](https://tautulli.com/) which can monitor your Plex server and send alerts [Tautulli](https://tautulli.com/) which can monitor your Plex server and send alerts
that way. that way.
### There's errors in my "tasks" tab! ### There are errors in my "tasks" tab!
You only really need to worry about these if there is a permanent failure. Some errors You only really need to worry about these if there is a permanent failure. Some errors
are temproary and will be retried for you automatically, such as a download got are temporary and will be retried for you automatically, such as a download got
interrupted and will be tried again later. Sources with permanet errors (such as no interrupted and will be tried again later. Sources with permanent errors (such as no
media available because you got a channel name wrong) will be shown as errors on the media available because you got a channel name wrong) will be shown as errors on the
"sources" tab. "sources" tab.
### What is TubeSync written in? ### What is TubeSync written in?
Python3 using Django, embedding youtube-dl. It's pretty much glue between other much Python3 using Django, embedding yt-dlp. It's pretty much glue between other much
larger libraries. larger libraries.
Notable libraries and software used: Notable libraries and software used:
* [Django](https://www.djangoproject.com/) * [Django](https://www.djangoproject.com/)
* [youtube-dl](https://yt-dl.org/) * [yt-dlp](https://github.com/yt-dlp/yt-dlp)
* [ffmpeg](https://ffmpeg.org/) * [ffmpeg](https://ffmpeg.org/)
* [Django Background Tasks](https://github.com/arteria/django-background-tasks/) * [Django Background Tasks](https://github.com/arteria/django-background-tasks/)
* [django-sass](https://github.com/coderedcorp/django-sass/) * [django-sass](https://github.com/coderedcorp/django-sass/)
@ -265,7 +325,7 @@ See the [Pipefile](https://github.com/meeb/tubesync/blob/main/Pipfile) for a ful
### Can I get access to the full Django admin? ### Can I get access to the full Django admin?
Yes, although pretty much all operations are available through the front end interface Yes, although pretty much all operations are available through the front-end interface
and you can probably break things by playing in the admin. If you still want to access and you can probably break things by playing in the admin. If you still want to access
it you can run: it you can run:
@ -278,7 +338,9 @@ can log in at http://localhost:4848/admin
### Are there user accounts or multi-user support? ### Are there user accounts or multi-user support?
No not at the moment. This could be added later if there is demand for it. There is support for basic HTTP authentication by setting the `HTTP_USER` and
`HTTP_PASS` environment variables. There is not support for multi-user or user
management.
### Does TubeSync support HTTPS? ### Does TubeSync support HTTPS?
@ -289,27 +351,37 @@ etc.). Configuration of this is beyond the scope of this README.
Just `amd64` for the moment. Others may be made available if there is demand. Just `amd64` for the moment. Others may be made available if there is demand.
### The pipenv install fails with "Locking failed"!
Make sure that you have `mysql_config` or `mariadb_config` available, as required by the python module `mysqlclient`. On Debian-based systems this is usually found in the package `libmysqlclient-dev`
# Advanced configuration # Advanced configuration
There are a number of other environment variables you can set. These are, mostly, There are a number of other environment variables you can set. These are, mostly,
**NOT** required to be set in the default container installation, they are mostly **NOT** required to be set in the default container installation, they are really only
useful if you are manually installing TubeSync in some other environment. These are: useful if you are manually installing TubeSync in some other environment. These are:
| Name | What | Example | | Name | What | Example |
| ----------------- | ------------------------------------- | ---------------------------------- | | --------------------------- | ------------------------------------------------------------ | ------------------------------------ |
| DJANGO_SECRET_KEY | Django secret key | YJySXnQLB7UVZw2dXKDWxI5lEZaImK6l | | DJANGO_SECRET_KEY | Django's SECRET_KEY | YJySXnQLB7UVZw2dXKDWxI5lEZaImK6l |
| TUBESYNC_DEBUG | Enable debugging | True | | DJANGO_URL_PREFIX | Run TubeSync in a sub-URL on the web server | /somepath/ |
| TUBESYNC_HOSTS | Django's ALLOWED_HOSTS | tubesync.example.com,otherhost.com | | TUBESYNC_DEBUG | Enable debugging | True |
| GUNICORN_WORKERS | Number of gunicorn workers to spawn | 3 | | TUBESYNC_WORKERS | Number of background workers, default is 2, max allowed is 8 | 2 |
| LISTEN_HOST | IP address for gunicorn to listen on | 127.0.0.1 | | TUBESYNC_HOSTS | Django's ALLOWED_HOSTS, defaults to `*` | tubesync.example.com,otherhost.com |
| LISTEN_PORT | Port number for gunicorn to listen on | 8080 | | TUBESYNC_RESET_DOWNLOAD_DIR | Toggle resetting `/downloads` permissions, defaults to True | True
| GUNICORN_WORKERS | Number of gunicorn workers to spawn | 3 |
| LISTEN_HOST | IP address for gunicorn to listen on | 127.0.0.1 |
| LISTEN_PORT | Port number for gunicorn to listen on | 8080 |
| HTTP_USER | Sets the username for HTTP basic authentication | some-username |
| HTTP_PASS | Sets the password for HTTP basic authentication | some-secure-password |
| DATABASE_CONNECTION | Optional external database connection details | mysql://user:pass@host:port/database |
# Manual, non-containerised, installation # Manual, non-containerised, installation
As a relatively normal Django app you can run TubeSync without the container. Beyond As a relatively normal Django app you can run TubeSync without the container. Beyond
the following rough guide you are on your own and should be knowledgeable about following this rough guide, you are on your own and should be knowledgeable about
installing and running WSGI-based Python web applications before attempting this. installing and running WSGI-based Python web applications before attempting this.
1. Clone or download this repo 1. Clone or download this repo
@ -320,7 +392,7 @@ installing and running WSGI-based Python web applications before attempting this
`tubesync/tubesync/local_settings.py` and edit it as appropriate `tubesync/tubesync/local_settings.py` and edit it as appropriate
5. Run migrations with `./manage.py migrate` 5. Run migrations with `./manage.py migrate`
6. Collect static files with `./manage.py collectstatic` 6. Collect static files with `./manage.py collectstatic`
6. Set up your prefered WSGI server, such as `gunicorn` poiting it to the application 6. Set up your prefered WSGI server, such as `gunicorn` pointing it to the application
in `tubesync/tubesync/wsgi.py` in `tubesync/tubesync/wsgi.py`
7. Set up your proxy server such as `nginx` and forward it to the WSGI server 7. Set up your proxy server such as `nginx` and forward it to the WSGI server
8. Check the web interface is working 8. Check the web interface is working
@ -332,7 +404,7 @@ installing and running WSGI-based Python web applications before attempting this
# Tests # Tests
There is a moderately comprehensive test suite focussing on the custom media format There is a moderately comprehensive test suite focusing on the custom media format
matching logic and that the front-end interface works. You can run it via Django: matching logic and that the front-end interface works. You can run it via Django:
```bash ```bash

View File

@ -1,27 +0,0 @@
#!/usr/bin/with-contenv bash
# Change runtime user UID and GID
PUID=${PUID:-911}
PGID=${PGID:-911}
groupmod -o -g "$PGID" app
usermod -o -u "$PUID" app
# Reset permissions
chown -R app:app /run/app && \
chmod -R 0700 /run/app && \
chown -R app:app /config && \
chmod -R 0755 /config && \
chown -R app:app /downloads && \
chmod -R 0755 /downloads && \
chown -R root:app /app && \
chmod -R 0750 /app && \
chown -R app:app /app/common/static && \
chmod -R 0750 /app/common/static && \
chown -R app:app /app/static && \
chmod -R 0750 /app/static && \
find /app -type f -exec chmod 640 {} \; && \
chmod +x /app/healthcheck.py
# Run migrations
exec s6-setuidgid app \
/usr/bin/python3 /app/manage.py migrate

View File

@ -79,6 +79,11 @@ http {
proxy_connect_timeout 10; proxy_connect_timeout 10;
} }
# File dwnload and streaming
location /media-data/ {
internal;
alias /downloads/;
}
} }
} }

View File

@ -0,0 +1,46 @@
bind 127.0.0.1
protected-mode yes
port 6379
tcp-backlog 511
timeout 0
tcp-keepalive 300
daemonize no
supervised no
loglevel notice
logfile ""
databases 1
always-show-logo no
save ""
dir /var/lib/redis
maxmemory 64mb
maxmemory-policy noeviction
lazyfree-lazy-eviction no
lazyfree-lazy-expire no
lazyfree-lazy-server-del no
replica-lazy-flush no
lazyfree-lazy-user-del no
oom-score-adj no
oom-score-adj-values 0 200 800
appendonly no
appendfsync no
lua-time-limit 5000
slowlog-log-slower-than 10000
slowlog-max-len 128
latency-monitor-threshold 0
notify-keyspace-events ""
hash-max-ziplist-entries 512
hash-max-ziplist-value 64
list-max-ziplist-size -2
list-compress-depth 0
set-max-intset-entries 512
zset-max-ziplist-entries 128
zset-max-ziplist-value 64
hll-sparse-max-bytes 3000
stream-node-max-bytes 4096
stream-node-max-entries 100
activerehashing yes
client-output-buffer-limit normal 0 0 0
client-output-buffer-limit replica 256mb 64mb 60
client-output-buffer-limit pubsub 32mb 8mb 60
hz 10
dynamic-hz yes

View File

@ -0,0 +1 @@
gunicorn

View File

@ -0,0 +1,25 @@
#!/usr/bin/with-contenv bash
UMASK_SET=${UMASK_SET:-022}
umask "$UMASK_SET"
cd /app || exit
PIDFILE=/run/app/celery-beat.pid
SCHEDULE=/tmp/tubesync-celerybeat-schedule
if [ -f "${PIDFILE}" ]
then
PID=$(cat $PIDFILE)
echo "Unexpected PID file exists at ${PIDFILE} with PID: ${PID}"
if kill -0 $PID
then
echo "Killing old gunicorn process with PID: ${PID}"
kill -9 $PID
fi
echo "Removing stale PID file: ${PIDFILE}"
rm ${PIDFILE}
fi
#exec s6-setuidgid app \
# /usr/local/bin/celery --workdir /app -A tubesync beat --pidfile ${PIDFILE} -s ${SCHEDULE}

View File

@ -0,0 +1 @@
longrun

View File

@ -0,0 +1 @@
gunicorn

View File

@ -0,0 +1,24 @@
#!/usr/bin/with-contenv bash
UMASK_SET=${UMASK_SET:-022}
umask "$UMASK_SET"
cd /app || exit
PIDFILE=/run/app/celery-worker.pid
if [ -f "${PIDFILE}" ]
then
PID=$(cat $PIDFILE)
echo "Unexpected PID file exists at ${PIDFILE} with PID: ${PID}"
if kill -0 $PID
then
echo "Killing old gunicorn process with PID: ${PID}"
kill -9 $PID
fi
echo "Removing stale PID file: ${PIDFILE}"
rm ${PIDFILE}
fi
#exec s6-setuidgid app \
# /usr/local/bin/celery --workdir /app -A tubesync worker --pidfile ${PIDFILE} -l INFO

View File

@ -0,0 +1 @@
longrun

View File

@ -0,0 +1 @@
tubesync-init

View File

@ -0,0 +1,24 @@
#!/command/with-contenv bash
UMASK_SET=${UMASK_SET:-022}
umask "$UMASK_SET"
cd /app || exit
PIDFILE=/run/app/gunicorn.pid
if [ -f "${PIDFILE}" ]
then
PID=$(cat $PIDFILE)
echo "Unexpected PID file exists at ${PIDFILE} with PID: ${PID}"
if kill -0 $PID
then
echo "Killing old gunicorn process with PID: ${PID}"
kill -9 $PID
fi
echo "Removing stale PID file: ${PIDFILE}"
rm ${PIDFILE}
fi
exec s6-setuidgid app \
/usr/local/bin/gunicorn -c /app/tubesync/gunicorn.py --capture-output tubesync.wsgi:application

View File

@ -0,0 +1 @@
longrun

View File

@ -0,0 +1 @@
gunicorn

View File

@ -0,0 +1,5 @@
#!/command/with-contenv bash
cd /
/usr/sbin/nginx

View File

@ -0,0 +1 @@
longrun

View File

@ -0,0 +1,4 @@
#!/command/with-contenv bash
exec s6-setuidgid redis \
/usr/bin/redis-server /etc/redis/redis.conf

View File

@ -0,0 +1 @@
longrun

View File

@ -0,0 +1,34 @@
#!/command/with-contenv bash
# Change runtime user UID and GID
PUID="${PUID:-911}"
PUID="${PUID:-911}"
groupmod -o -g "$PGID" app
usermod -o -u "$PUID" app
# Reset permissions
chown -R app:app /run/app
chmod -R 0700 /run/app
chown -R app:app /config
chmod -R 0755 /config
chown -R root:app /app
chmod -R 0750 /app
chown -R app:app /app/common/static
chmod -R 0750 /app/common/static
chown -R app:app /app/static
chmod -R 0750 /app/static
find /app -type f ! -iname healthcheck.py -exec chmod 640 {} \;
chmod 0755 /app/healthcheck.py
# Optionally reset the download dir permissions
TUBESYNC_RESET_DOWNLOAD_DIR="${TUBESYNC_RESET_DOWNLOAD_DIR:-True}"
if [ "$TUBESYNC_RESET_DOWNLOAD_DIR" == "True" ]
then
echo "TUBESYNC_RESET_DOWNLOAD_DIR=True, Resetting /downloads directory permissions"
chown -R app:app /downloads
chmod -R 0755 /downloads
fi
# Run migrations
exec s6-setuidgid app \
/usr/bin/python3 /app/manage.py migrate

View File

@ -0,0 +1 @@
oneshot

View File

@ -0,0 +1,3 @@
#!/command/execlineb -P
/etc/s6-overlay/s6-rc.d/tubesync-init/run

View File

@ -0,0 +1 @@
gunicorn

View File

@ -1,4 +1,4 @@
#!/usr/bin/with-contenv bash #!/command/with-contenv bash
exec s6-setuidgid app \ exec s6-setuidgid app \
/usr/bin/python3 /app/manage.py process_tasks /usr/bin/python3 /app/manage.py process_tasks

View File

@ -0,0 +1 @@
longrun

View File

@ -1,9 +0,0 @@
#!/usr/bin/with-contenv bash
UMASK_SET=${UMASK_SET:-022}
umask "$UMASK_SET"
cd /app || exit
exec s6-setuidgid app \
/usr/local/bin/gunicorn -c /app/tubesync/gunicorn.py --capture-output tubesync.wsgi:application

View File

@ -1,5 +0,0 @@
#!/usr/bin/with-contenv bash
cd /
/usr/sbin/nginx

View File

@ -0,0 +1,37 @@
# TubeSync
## Advanced usage guide - creating missing metadata
This is a new feature in v0.9 of TubeSync and later. It allows you to create or
re-create missing metadata in your TubeSync download directories for missing `nfo`
files and thumbnails.
If you add a source with "write NFO files" or "copy thumbnails" disabled, download
some media and then update the source to write NFO files or copy thumbnails then
TubeSync will not automatically retroactively attempt to copy or create your missing
metadata files. You can use a special one-off command to manually write missing
metadata files to the correct locations.
## Requirements
You have added a source without metadata writing enabled, downloaded some media, then
updated the source to enable metadata writing.
## Steps
### 1. Run the batch metadata sync command
Execute the following Django command:
`./manage.py sync-missing-metadata`
When deploying TubeSync inside a container, you can execute this with:
`docker exec -ti tubesync python3 /app/manage.py sync-missing-metadata`
This command will log what its doing to the terminal when you run it.
Internally, this command loops over all your sources which have been saved with
"write NFO files" or "copy thumbnails" enabled. Then, loops over all media saved to
that source and confirms that the appropriate thumbnail files have been copied over and
the NFO file has been written if enabled.

BIN
docs/dashboard-v0.5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 170 KiB

View File

@ -0,0 +1,81 @@
# TubeSync
## Advanced usage guide - importing existing media
This is a new feature in v0.9 of TubeSync and later. It allows you to mark existing
downloaded media as "downloaded" in TubeSync. You can use this feature if, for example,
you already have an extensive catalogue of downloaded media which you want to mark
as downloaded into TubeSync so TubeSync doesn't re-download media you already have.
## Requirements
Your existing downloaded media MUST contain the unique ID. For YouTube videos, this is
means the YouTube video ID MUST be in the filename.
Supported extensions to be imported are .m4a, .ogg, .mkv, .mp3, .mp4 and .avi. Your
media you want to import must end in one of these file extensions.
## Caveats
As TubeSync does not probe media and your existing media may be re-encoded or in
different formats to what is available in the current media metadata there is no way
for TubeSync to know what codecs, resolution, bitrate etc. your imported media is in.
Any manually imported existing local media will display blank boxes for this
information on the TubeSync interface as it's unavailable.
## Steps
### 1. Add your source to TubeSync
Add your source to TubeSync, such as a YouTube channel. **Make sure you untick the
"download media" checkbox.**
This will allow TubeSync to index all the available media on your source, but won't
start downloading any media.
### 2. Wait
Wait for all the media on your source to be indexed. This may take some time.
### 3. Move your existing media into TubeSync
You now need to move your existing media into TubeSync. You need to move the media
files into the correct download directories created by TubeSync. For example, if you
have downloaded videos for a YouTube channel "TestChannel", you would have added this
as a source called TestChannel and in a directory called test-channel in Tubesync. It
would have a download directory created on disk at:
`/path/to/downloads/test-channel`
You would move all of your pre-existing videos you downloaded outside of TubeSync for
this channel into this directory.
In short, your existing media needs to be moved into the correct TubeSync source
directory to be detected.
This is required so TubeSync can known which Source to link the media to.
### 4. Run the batch import command
Execute the following Django command:
`./manage.py import-existing-media`
When deploying TubeSync inside a container, you can execute this with:
`docker exec -ti tubesync python3 /app/manage.py import-existing-media`
This command will log what its doing to the terminal when you run it.
Internally, `import-existing-media` looks for the unique media key (for YouTube, this
is the YouTube video ID) in the filename and detects the source to link it to based
on the directory the media file is inside.
### 5. Re-enable downloading at the source
Edit your source and re-enable / tick the "download media" option. This will allow
TubeSync to download any missing media you did not manually import.
Note that TubeSync will still get screenshots write `nfo` files etc. for files you
manually import if enabled at the source level.

BIN
docs/media-item-v0.5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 361 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 420 KiB

BIN
docs/media-v0.5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 666 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 530 KiB

View File

@ -0,0 +1,132 @@
# TubeSync
## Advanced usage guide - using other database backends
This is a new feature in v1.0 of TubeSync and later. It allows you to use a custom
existing external database server instead of the default SQLite database. You may want
to use this if you encounter performance issues with adding very large or a large
number of channels and database write contention (as shown by errors in the log)
become an issue.
## Requirements
TubeSync supports SQLite (the automatic default) as well as PostgreSQL, MySQL and
MariaDB. For MariaDB just follow the MySQL instructions as the driver is the same.
You should start with a blank install of TubeSync. Migrating to a new database will
reset your database. If you are comfortable with Django you can export and re-import
existing database data with:
```bash
$ docker exec -i tubesync python3 /app/manage.py dumpdata > some-file.json
```
Then change you database backend over, then use
```bash
$ cat some-file.json | docker exec -i tubesync python3 /app/manage.py loaddata - --format=json
```
As detailed in the Django documentation:
https://docs.djangoproject.com/en/3.1/ref/django-admin/#dumpdata
and:
https://docs.djangoproject.com/en/3.1/ref/django-admin/#loaddata
Further instructions are beyond the scope of TubeSync documenation and you should refer
to Django documentation for more details.
If you are not comfortable with the above, then skip the `dumpdata` steps, however
remember you will start again with a completely new database.
## Steps
### 1. Create a database in your external database server
You need to create a database and a user with permissions to access the database in
your chosen external database server. Steps vary between PostgreSQL, MySQL and MariaDB
so this is up to you to work out.
### 2. Set the database connection string environment variable
You need to provide the database connection details to TubeSync via an environment
variable. The environment variable name is `DATABASE_CONNECTION` and the format is the
standard URL-style string. Examples are:
`postgresql://tubesync:password@localhost:5432/tubesync`
and
`mysql://tubesync:password@localhost:3306/tubesync`
*Important note:* For MySQL databases make SURE you create the tubesync database with
`utf8mb4` encoding, like:
`CREATE DATABASE tubesync CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;`
Without `utf8mb4` encoding things like emojis in video titles (or any extended UTF8
characters) can cause issues.
### 3. Start TubeSync and check the logs
Once you start TubeSync with the new database connection you should see the folling log
entry in the container or stdout logs:
`2021-04-04 22:42:17,912 [tubesync/INFO] Using database connection: django.db.backends.postgresql://tubesync:[hidden]@localhost:5432/tubesync`
If you see a line similar to the above and the web interface loads, congratulations,
you are now using an external database server for your TubeSync data!
## Database Compression (For MariaDB)
With a lot of media files the `sync_media` table grows in size quickly.
You can save space using column compression using the following steps while using MariaDB:
1. Stop tubesync
2. Execute `ALTER TABLE sync_media MODIFY metadata LONGTEXT COMPRESSED;` on database tubesync
3. Start tunesync and confirm the connection still works.
## Docker Compose
If you're using Docker Compose and simply want to connect to another container with
the DB for the performance benefits, a configuration like this would be enough:
```
tubesync-db:
image: postgres:15.2
container_name: tubesync-db
restart: unless-stopped
volumes:
- /<path/to>/init.sql:/docker-entrypoint-initdb.d/init.sql
- /<path/to>/tubesync-db:/var/lib/postgresql/data
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=testpassword
tubesync:
image: ghcr.io/meeb/tubesync:latest
container_name: tubesync
restart: unless-stopped
ports:
- 4848:4848
volumes:
- /<path/to>/tubesync/config:/config
- /<path/to>/YouTube:/downloads
environment:
- DATABASE_CONNECTION=postgresql://postgres:testpassword@tubesync-db:5432/tubesync
depends_on:
- tubesync-db
```
Note that an `init.sql` file is needed to initialize the `tubesync`
database before it can be written to. This file should contain:
```
CREATE DATABASE tubesync;
```
Then it must be mapped to `/docker-entrypoint-initdb.d/init.sql` for it
to be executed on first startup of the container. See the `tubesync-db`
volume mapping above for how to do this.

30
docs/reset-metadata.md Normal file
View File

@ -0,0 +1,30 @@
# TubeSync
## Advanced usage guide - reset media metadata from the command line
This command allows you to reset all media item metadata. You might want to use
this if you have a lot of media items with invalid metadata and you want to
wipe it which triggers the metadata to be redownloaded.
## Requirements
You have added some sources and media
## Steps
### 1. Run the reset tasks command
Execute the following Django command:
`./manage.py reset-metadata`
When deploying TubeSync inside a container, you can execute this with:
`docker exec -ti tubesync python3 /app/manage.py reset-metadata`
This command will log what its doing to the terminal when you run it.
When this is run, new tasks will be immediately created so all your media
items will start downloading updated metadata straight away, any missing information
such as thumbnails will be redownloaded, etc.

33
docs/reset-tasks.md Normal file
View File

@ -0,0 +1,33 @@
# TubeSync
## Advanced usage guide - reset tasks from the command line
This is a new feature in v1.0 of TubeSync and later. It allows you to reset all
scheduled tasks from the command line as well as the "reset tasks" button in the
"tasks" tab of the dashboard.
This is useful for TubeSync installations where you may have a lot of media and
sources added and the "reset tasks" button may take too long to the extent where
the page times out (with a 502 error or similar issue).
## Requirements
You have added some sources and media
## Steps
### 1. Run the reset tasks command
Execute the following Django command:
`./manage.py reset-tasks`
When deploying TubeSync inside a container, you can execute this with:
`docker exec -ti tubesync python3 /app/manage.py reset-tasks`
This command will log what its doing to the terminal when you run it.
When this is run, new tasks will be immediately created so all your sources will be
indexed again straight away, any missing information such as thumbnails will be
redownloaded, etc.

BIN
docs/source-v0.5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 137 KiB

BIN
docs/sources-v0.5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

50
docs/using-cookies.md Normal file
View File

@ -0,0 +1,50 @@
# TubeSync
## Advanced usage guide - using exported cookies
This is a new feature in v0.10 of TubeSync and later. It allows you to use the cookies
file exported from your browser in "Netscape" format with TubeSync to authenticate
to YouTube. This can bypass some throttling, age restrictions and other blocks at
YouTube.
**IMPORTANT NOTE**: Using cookies exported from your browser that is authenticated
to YouTube identifes your Google account as using TubeSync. This may result in
potential account impacts and is entirely at your own risk. Do not use this
feature unless you really know what you're doing.
## Requirements
Have a browser that supports exporting your cookies and be logged into YouTube.
## Steps
### 1. Export your cookies
You need to export cookies for youtube.com from your browser, you can either do
this manually or there are plug-ins to automate this for you. This file must be
in the "Netscape" cookie export format.
Save your cookies as a `cookies.txt` file.
### 2. Import into TubeSync
Drop the `cookies.txt` file into your TubeSync `config` directory.
If detected correctly, you will see something like this in the worker or container
logs:
```
YYYY-MM-DD HH:MM:SS,mmm [tubesync/INFO] [youtube-dl] using cookies.txt from: /config/cookies.txt
```
If you see that line it's working correctly.
If you see errors in your logs like this:
```
http.cookiejar.LoadError: '/config/cookies.txt' does not look like a Netscape format cookies file
```
Then your `cookies.txt` file was not generated or created correctly as it's not
in the required "Netscape" format. You can fix this by exporting your `cookies.txt`
in the correct "Netscape" format.

2
pip.conf Normal file
View File

@ -0,0 +1,2 @@
[global]
extra-index-url=https://www.piwheels.org/simple

View File

@ -1,10 +1,10 @@
from django.conf import settings from django.conf import settings
from .third_party_versions import youtube_dl_version, ffmpeg_version from .third_party_versions import yt_dlp_version, ffmpeg_version
def app_details(request): def app_details(request):
return { return {
'app_version': str(settings.VERSION), 'app_version': str(settings.VERSION),
'youtube_dl_version': youtube_dl_version, 'yt_dlp_version': yt_dlp_version,
'ffmpeg_version': ffmpeg_version, 'ffmpeg_version': ffmpeg_version,
} }

View File

@ -20,3 +20,10 @@ class DownloadFailedException(Exception):
exist. exist.
''' '''
pass pass
class DatabaseConnectionError(Exception):
'''
Raised when parsing or initially connecting to a database.
'''
pass

View File

@ -1,10 +1,14 @@
import logging import logging
from django.conf import settings
logging_level = logging.DEBUG if settings.DEBUG else logging.INFO
log = logging.getLogger('tubesync') log = logging.getLogger('tubesync')
log.setLevel(logging.DEBUG) log.setLevel(logging_level)
ch = logging.StreamHandler() ch = logging.StreamHandler()
ch.setLevel(logging.DEBUG) ch.setLevel(logging_level)
formatter = logging.Formatter('%(asctime)s [%(name)s/%(levelname)s] %(message)s') formatter = logging.Formatter('%(asctime)s [%(name)s/%(levelname)s] %(message)s')
ch.setFormatter(formatter) ch.setFormatter(formatter)
log.addHandler(ch) log.addHandler(ch)

View File

@ -1,4 +1,6 @@
from django.conf import settings
from django.forms import BaseForm from django.forms import BaseForm
from basicauth.middleware import BasicAuthMiddleware as BaseBasicAuthMiddleware
class MaterializeDefaultFieldsMiddleware: class MaterializeDefaultFieldsMiddleware:
@ -19,3 +21,12 @@ class MaterializeDefaultFieldsMiddleware:
for _, field in v.fields.items(): for _, field in v.fields.items():
field.widget.attrs.update({'class':'browser-default'}) field.widget.attrs.update({'class':'browser-default'})
return response return response
class BasicAuthMiddleware(BaseBasicAuthMiddleware):
def process_request(self, request):
bypass_uris = getattr(settings, 'BASICAUTH_ALWAYS_ALLOW_URIS', [])
if request.path in bypass_uris:
return None
return super().process_request(request)

View File

@ -1,20 +1,20 @@
@font-face { @font-face {
font-family: 'roboto-light'; font-family: 'roboto';
src: url('/static/fonts/roboto/roboto-light.woff') format('woff'); src: url('../fonts/roboto/roboto-light.woff') format('woff');
font-weight: lighter;
font-style: normal;
}
@font-face {
font-family: 'roboto';
src: url('../fonts/roboto/roboto-regular.woff') format('woff');
font-weight: normal; font-weight: normal;
font-style: normal; font-style: normal;
} }
@font-face { @font-face {
font-family: 'roboto-regular'; font-family: 'roboto';
src: url('/static/fonts/roboto/roboto-regular.woff') format('woff'); src: url('../fonts/roboto/roboto-bold.woff') format('woff');
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: 'roboto-bold';
src: url('/static/fonts/roboto/roboto-bold.woff') format('woff');
font-weight: bold; font-weight: bold;
font-style: normal; font-style: normal;
} }

View File

@ -4,7 +4,7 @@
} }
.help-text { .help-text {
color: $form-help-text-colour; color: $form-help-text-colour;
padding: 1rem 0 1rem 0; padding-bottom: 1rem;
} }
label { label {
text-transform: uppercase; text-transform: uppercase;

View File

@ -5,6 +5,13 @@ html {
color: $text-colour; color: $text-colour;
} }
body {
display: flex;
min-height: 100vh;
flex-direction: column;
justify-content: space-between;
}
header { header {
background-color: $header-background-colour; background-color: $header-background-colour;
@ -174,8 +181,10 @@ main {
display: inline-block; display: inline-block;
font-weight: bold; font-weight: bold;
text-decoration: none; text-decoration: none;
padding: 5px 10px 5px 10px; padding: 5px 8px 4px 8px;
margin: 0 3px 0 3px; margin: 0 3px 6px 3px;
min-width: 40px;
min-height: 40px;
background-color: $pagination-background-colour; background-color: $pagination-background-colour;
color: $pagination-text-colour; color: $pagination-text-colour;
border: 2px $pagination-border-colour solid; border: 2px $pagination-border-colour solid;

View File

@ -1,2 +1,2 @@
$font-family: 'roboto-regular', Arial, Helvetica, sans-serif; $font-family: 'roboto', Arial, Helvetica, sans-serif;
$font-size: 1.05rem; $font-size: 1.05rem;

View File

@ -65,6 +65,7 @@ readers do not read off random characters that represent icons */
.#{$fa-css-prefix}-arrows-alt-h:before { content: fa-content($fa-var-arrows-alt-h); } .#{$fa-css-prefix}-arrows-alt-h:before { content: fa-content($fa-var-arrows-alt-h); }
.#{$fa-css-prefix}-arrows-alt-v:before { content: fa-content($fa-var-arrows-alt-v); } .#{$fa-css-prefix}-arrows-alt-v:before { content: fa-content($fa-var-arrows-alt-v); }
.#{$fa-css-prefix}-artstation:before { content: fa-content($fa-var-artstation); } .#{$fa-css-prefix}-artstation:before { content: fa-content($fa-var-artstation); }
.#{$fa-css-prefix}-arrow-rotate-right:before { content: fa-content($fa-var-arrow-rotate-right); }
.#{$fa-css-prefix}-assistive-listening-systems:before { content: fa-content($fa-var-assistive-listening-systems); } .#{$fa-css-prefix}-assistive-listening-systems:before { content: fa-content($fa-var-assistive-listening-systems); }
.#{$fa-css-prefix}-asterisk:before { content: fa-content($fa-var-asterisk); } .#{$fa-css-prefix}-asterisk:before { content: fa-content($fa-var-asterisk); }
.#{$fa-css-prefix}-asymmetrik:before { content: fa-content($fa-var-asymmetrik); } .#{$fa-css-prefix}-asymmetrik:before { content: fa-content($fa-var-asymmetrik); }

View File

@ -80,6 +80,7 @@ $fa-var-arrow-right: \f061;
$fa-var-arrow-up: \f062; $fa-var-arrow-up: \f062;
$fa-var-arrows-alt: \f0b2; $fa-var-arrows-alt: \f0b2;
$fa-var-arrows-alt-h: \f337; $fa-var-arrows-alt-h: \f337;
$fa-var-arrow-rotate-right: \f01e;
$fa-var-arrows-alt-v: \f338; $fa-var-arrows-alt-v: \f338;
$fa-var-artstation: \f77a; $fa-var-artstation: \f77a;
$fa-var-assistive-listening-systems: \f2a2; $fa-var-assistive-listening-systems: \f2a2;

View File

@ -14,7 +14,7 @@
// Text Label Style // Text Label Style
+ span:not(.lever) { + span:not(.lever) {
position: relative; position: relative;
padding-left: 35px; padding-left: 27px;
cursor: pointer; cursor: pointer;
display: inline-block; display: inline-block;
height: 25px; height: 25px;

View File

@ -17,3 +17,16 @@ html {
visibility: visible; visibility: visible;
opacity: 1; opacity: 1;
} }
.flex-collection-container {
display: flex !important;
align-items: center;
}
.flex-grow {
flex-grow: 1;
}
.help-text > i {
padding-right: 6px;
}

View File

@ -16,32 +16,36 @@
<body> <body>
<header> <div class="app">
<div class="container">
<a href="{% url 'sync:dashboard' %}">
{% include 'tubesync.svg' with width='3rem' height='3rem' %}
<h1>TubeSync</h1>
</a>
</div>
</header>
<nav> <header>
<div class="container"> <div class="container">
<ul> <a href="{% url 'sync:dashboard' %}">
<li><a href="{% url 'sync:dashboard' %}"><i class="fas fa-fw fa-th-large"></i><span class="hide-on-med-and-down"> Dashboard</span></a></li> {% include 'tubesync.svg' with width='3rem' height='3rem' %}
<li><a href="{% url 'sync:sources' %}"><i class="fas fa-fw fa-play"></i><span class="hide-on-med-and-down"> Sources</span></a></li> <h1>TubeSync</h1>
<li><a href="{% url 'sync:media' %}"><i class="fas fa-fw fa-film"></i><span class="hide-on-med-and-down"> Media</span></a></li> </a>
<li><a href="{% url 'sync:tasks' %}"><i class="far fa-fw fa-clock"></i><span class="hide-on-med-and-down"> Tasks</span></a></li> </div>
<li><a href="{% url 'sync:mediaservers' %}"><i class="fas fa-fw fa-stream"></i><span class="hide-on-med-and-down"> Media Servers</span></a></li> </header>
</ul>
</div>
</nav>
<main> <nav>
<div class="container"> <div class="container">
{% block content %}{% endblock %} <ul>
</div> <li><a href="{% url 'sync:dashboard' %}"><i class="fas fa-fw fa-th-large"></i><span class="hide-on-med-and-down"> Dashboard</span></a></li>
</main> <li><a href="{% url 'sync:sources' %}"><i class="fas fa-fw fa-play"></i><span class="hide-on-med-and-down"> Sources</span></a></li>
<li><a href="{% url 'sync:media' %}"><i class="fas fa-fw fa-film"></i><span class="hide-on-med-and-down"> Media</span></a></li>
<li><a href="{% url 'sync:tasks' %}"><i class="far fa-fw fa-clock"></i><span class="hide-on-med-and-down"> Tasks</span></a></li>
<li><a href="{% url 'sync:mediaservers' %}"><i class="fas fa-fw fa-stream"></i><span class="hide-on-med-and-down"> Media Servers</span></a></li>
</ul>
</div>
</nav>
<main>
<div class="container">
{% block content %}{% endblock %}
</div>
</main>
</div>
<footer> <footer>
<div class="container"> <div class="container">
@ -53,7 +57,7 @@
</p> </p>
<p> <p>
<a href="https://github.com/meeb/tubesync" class="nowrap" target="_blank"><i class="fab fa-github"></i> TubeSync</a> version <strong>{{ app_version }}</strong> with <a href="https://github.com/meeb/tubesync" class="nowrap" target="_blank"><i class="fab fa-github"></i> TubeSync</a> version <strong>{{ app_version }}</strong> with
<a href="https://yt-dl.org/" class="nowrap" target="_blank"><i class="fas fa-link"></i> youtube-dl</a> version <strong>{{ youtube_dl_version }}</strong> and <a href="https://github.com/yt-dlp/yt-dlp" class="nowrap" target="_blank"><i class="fas fa-link"></i> yt-dlp</a> version <strong>{{ yt_dlp_version }}</strong> and
<a href="https://ffmpeg.org/" class="nowrap" target="_blank"><i class="fas fa-link"></i> FFmpeg</a> version <strong>{{ ffmpeg_version }}</strong>. <a href="https://ffmpeg.org/" class="nowrap" target="_blank"><i class="fas fa-link"></i> FFmpeg</a> version <strong>{{ ffmpeg_version }}</strong>.
</p> </p>
</div> </div>

View File

@ -3,7 +3,7 @@
<div class="col s12"> <div class="col s12">
<div class="pagination"> <div class="pagination">
{% for i in paginator.page_range %} {% for i in paginator.page_range %}
<a class="pagenum{% if i == page_obj.number %} currentpage{% endif %}" href="?{% if filter %}filter={{ filter }}&{% endif %}page={{ i }}">{{ i }}</a> <a class="pagenum{% if i == page_obj.number %} currentpage{% endif %}" href="?{% if filter %}filter={{ filter }}&{% endif %}page={{ i }}{% if show_skipped %}&show_skipped=yes{% endif %}{% if only_skipped %}&only_skipped=yes{% endif %}">{{ i }}</a>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>

View File

@ -2,6 +2,8 @@ import os.path
from django.conf import settings from django.conf import settings
from django.test import TestCase, Client from django.test import TestCase, Client
from .testutils import prevent_request_warnings from .testutils import prevent_request_warnings
from .utils import parse_database_connection_string, clean_filename
from .errors import DatabaseConnectionError
class ErrorPageTestCase(TestCase): class ErrorPageTestCase(TestCase):
@ -61,3 +63,75 @@ class CommonStaticTestCase(TestCase):
favicon_real_path = os.path.join(os.sep.join(root_parts), favicon_real_path = os.path.join(os.sep.join(root_parts),
os.sep.join(url_parts)) os.sep.join(url_parts))
self.assertTrue(os.path.exists(favicon_real_path)) self.assertTrue(os.path.exists(favicon_real_path))
class UtilsTestCase(TestCase):
def test_parse_database_connection_string(self):
database_dict = parse_database_connection_string(
'postgresql://tubesync:password@localhost:5432/tubesync')
self.assertEqual(database_dict,
{
'DRIVER': 'postgresql',
'ENGINE': 'django.db.backends.postgresql',
'USER': 'tubesync',
'PASSWORD': 'password',
'HOST': 'localhost',
'PORT': 5432,
'NAME': 'tubesync',
'CONN_MAX_AGE': 300,
'OPTIONS': {},
}
)
database_dict = parse_database_connection_string(
'mysql://tubesync:password@localhost:3306/tubesync')
self.assertEqual(database_dict,
{
'DRIVER': 'mysql',
'ENGINE': 'django.db.backends.mysql',
'USER': 'tubesync',
'PASSWORD': 'password',
'HOST': 'localhost',
'PORT': 3306,
'NAME': 'tubesync',
'CONN_MAX_AGE': 300,
'OPTIONS': {'charset': 'utf8mb4'}
}
)
# Invalid driver
with self.assertRaises(DatabaseConnectionError):
parse_database_connection_string(
'test://tubesync:password@localhost:5432/tubesync')
# No username
with self.assertRaises(DatabaseConnectionError):
parse_database_connection_string(
'postgresql://password@localhost:5432/tubesync')
# No database name
with self.assertRaises(DatabaseConnectionError):
parse_database_connection_string(
'postgresql://tubesync:password@5432')
# Invalid port
with self.assertRaises(DatabaseConnectionError):
parse_database_connection_string(
'postgresql://tubesync:password@localhost:test/tubesync')
# Invalid port
with self.assertRaises(DatabaseConnectionError):
parse_database_connection_string(
'postgresql://tubesync:password@localhost:65537/tubesync')
# Invalid username or password
with self.assertRaises(DatabaseConnectionError):
parse_database_connection_string(
'postgresql://tubesync:password:test@localhost:5432/tubesync')
# Invalid database name
with self.assertRaises(DatabaseConnectionError):
parse_database_connection_string(
'postgresql://tubesync:password@localhost:5432/tubesync/test')
def test_clean_filename(self):
self.assertEqual(clean_filename('a'), 'a')
self.assertEqual(clean_filename('a\t'), 'a')
self.assertEqual(clean_filename('a\n'), 'a')
self.assertEqual(clean_filename('a a'), 'a a')
self.assertEqual(clean_filename('a a'), 'a a')
self.assertEqual(clean_filename('a\t\t\ta'), 'a a')
self.assertEqual(clean_filename('a\t\t\ta\t\t\t'), 'a a')

View File

@ -1,7 +1,7 @@
from youtube_dl import version as yt_version from yt_dlp import version as yt_dlp_version
youtube_dl_version = str(yt_version.__version__) yt_dlp_version = str(yt_dlp_version.__version__)
ffmpeg_version = '(shared install)' ffmpeg_version = '(shared install)'

View File

@ -1,4 +1,96 @@
from urllib.parse import urlunsplit, urlencode import string
from datetime import datetime
from urllib.parse import urlunsplit, urlencode, urlparse
from yt_dlp.utils import LazyList
from .errors import DatabaseConnectionError
def parse_database_connection_string(database_connection_string):
'''
Parses a connection string in a URL style format, such as:
postgresql://tubesync:password@localhost:5432/tubesync
mysql://someuser:somepassword@localhost:3306/tubesync
into a Django-compatible settings.DATABASES dict format.
'''
valid_drivers = ('postgresql', 'mysql')
default_ports = {
'postgresql': 5432,
'mysql': 3306,
}
django_backends = {
'postgresql': 'django.db.backends.postgresql',
'mysql': 'django.db.backends.mysql',
}
backend_options = {
'postgresql': {},
'mysql': {
'charset': 'utf8mb4',
}
}
try:
parts = urlparse(str(database_connection_string))
except Exception as e:
raise DatabaseConnectionError(f'Failed to parse "{database_connection_string}" '
f'as a database connection string: {e}') from e
driver = parts.scheme
user_pass_host_port = parts.netloc
database = parts.path
if driver not in valid_drivers:
raise DatabaseConnectionError(f'Database connection string '
f'"{database_connection_string}" specified an '
f'invalid driver, must be one of {valid_drivers}')
django_driver = django_backends.get(driver)
host_parts = user_pass_host_port.split('@')
if len(host_parts) != 2:
raise DatabaseConnectionError(f'Database connection string netloc must be in '
f'the format of user:pass@host')
user_pass, host_port = host_parts
user_pass_parts = user_pass.split(':')
if len(user_pass_parts) != 2:
raise DatabaseConnectionError(f'Database connection string netloc must be in '
f'the format of user:pass@host')
username, password = user_pass_parts
host_port_parts = host_port.split(':')
if len(host_port_parts) == 1:
# No port number, assign a default port
hostname = host_port_parts[0]
port = default_ports.get(driver)
elif len(host_port_parts) == 2:
# Host name and port number
hostname, port = host_port_parts
try:
port = int(port)
except (ValueError, TypeError) as e:
raise DatabaseConnectionError(f'Database connection string contained an '
f'invalid port, ports must be integers: '
f'{e}') from e
if not 0 < port < 63336:
raise DatabaseConnectionError(f'Database connection string contained an '
f'invalid port, ports must be between 1 and '
f'65535, got {port}')
else:
# Malformed
raise DatabaseConnectionError(f'Database connection host must be a hostname or '
f'a hostname:port combination')
if database.startswith('/'):
database = database[1:]
if not database:
raise DatabaseConnectionError(f'Database connection string path must be a '
f'string in the format of /databasename')
if '/' in database:
raise DatabaseConnectionError(f'Database connection string path can only '
f'contain a single string name, got: {database}')
return {
'DRIVER': driver,
'ENGINE': django_driver,
'NAME': database,
'USER': username,
'PASSWORD': password,
'HOST': hostname,
'PORT': port,
'CONN_MAX_AGE': 300,
'OPTIONS': backend_options.get(driver),
}
def get_client_ip(request): def get_client_ip(request):
@ -14,3 +106,26 @@ def append_uri_params(uri, params):
uri = str(uri) uri = str(uri)
qs = urlencode(params) qs = urlencode(params)
return urlunsplit(('', '', uri, qs, '')) return urlunsplit(('', '', uri, qs, ''))
def clean_filename(filename):
if not isinstance(filename, str):
raise ValueError(f'filename must be a str, got {type(filename)}')
to_scrub = '<>\/:*?"|%'
for char in to_scrub:
filename = filename.replace(char, '')
clean_filename = ''
for c in filename:
if c in string.whitespace:
c = ' '
if ord(c) > 30:
clean_filename += c
return clean_filename.strip()
def json_serial(obj):
if isinstance(obj, datetime):
return obj.isoformat()
if isinstance(obj, LazyList):
return list(obj)
raise TypeError(f'Type {type(obj)} is not json_serial()-able')

0
tubesync/healthcheck.py Normal file → Executable file
View File

View File

@ -7,7 +7,7 @@ class SourceAdmin(admin.ModelAdmin):
ordering = ('-created',) ordering = ('-created',)
list_display = ('uuid', 'name', 'source_type', 'last_crawl', list_display = ('uuid', 'name', 'source_type', 'last_crawl',
'has_failed') 'download_media', 'has_failed')
readonly_fields = ('uuid', 'created') readonly_fields = ('uuid', 'created')
search_fields = ('uuid', 'key', 'name') search_fields = ('uuid', 'key', 'name')

109
tubesync/sync/fields.py Normal file
View File

@ -0,0 +1,109 @@
from django.forms import MultipleChoiceField, CheckboxSelectMultiple, Field, TypedMultipleChoiceField
from django.db import models
from typing import Any, Optional, Dict
from django.utils.translation import gettext_lazy as _
# this is a form field!
class CustomCheckboxSelectMultiple(CheckboxSelectMultiple):
template_name = 'widgets/checkbox_select.html'
option_template_name = 'widgets/checkbox_option.html'
def get_context(self, name: str, value: Any, attrs) -> Dict[str, Any]:
ctx = super().get_context(name, value, attrs)['widget']
ctx["multipleChoiceProperties"] = []
for _group, options, _index in ctx["optgroups"]:
for option in options:
if not isinstance(value,str) and not isinstance(value,list) and ( option["value"] in value.selected_choices or ( value.allow_all and value.all_choice in value.selected_choices ) ):
checked = True
else:
checked = False
ctx["multipleChoiceProperties"].append({
"template_name": option["template_name"],
"type": option["type"],
"value": option["value"],
"label": option["label"],
"name": option["name"],
"checked": checked})
return { 'widget': ctx }
# this is a database field!
class CommaSepChoiceField(models.Field):
"Implements comma-separated storage of lists"
def __init__(self, separator=",", possible_choices=(("","")), all_choice="", all_label="All", allow_all=False, *args, **kwargs):
self.separator = separator
self.possible_choices = possible_choices
self.selected_choices = []
self.allow_all = allow_all
self.all_label = all_label
self.all_choice = all_choice
super().__init__(*args, **kwargs)
def deconstruct(self):
name, path, args, kwargs = super().deconstruct()
if self.separator != ",":
kwargs['separator'] = self.separator
kwargs['possible_choices'] = self.possible_choices
return name, path, args, kwargs
def db_type(self, connection):
return 'text'
def get_my_choices(self):
choiceArray = []
if self.possible_choices is None:
return choiceArray
if self.allow_all:
choiceArray.append((self.all_choice, _(self.all_label)))
for t in self.possible_choices:
choiceArray.append(t)
return choiceArray
def formfield(self, **kwargs):
# This is a fairly standard way to set up some defaults
# while letting the caller override them.
defaults = {'form_class': MultipleChoiceField,
'choices': self.get_my_choices,
'widget': CustomCheckboxSelectMultiple,
'label': '',
'required': False}
defaults.update(kwargs)
#del defaults.required
return super().formfield(**defaults)
def deconstruct(self):
name, path, args, kwargs = super().deconstruct()
# Only include kwarg if it's not the default
if self.separator != ",":
kwargs['separator'] = self.separator
return name, path, args, kwargs
def from_db_value(self, value, expr, conn):
if value is None:
self.selected_choices = []
else:
self.selected_choices = value.split(",")
return self
def get_prep_value(self, value):
if value is None:
return ""
if not isinstance(value,list):
return ""
if self.all_choice not in value:
return ",".join(value)
else:
return self.all_choice
def get_text_for_value(self, val):
fval = [i for i in self.possible_choices if i[0] == val]
if len(fval) <= 0:
return []
else:
return fval[0][1]

View File

View File

@ -0,0 +1,51 @@
import os
import uuid
from django.utils.translation import gettext_lazy as _
from django.core.management.base import BaseCommand, CommandError
from django.db.models import signals
from common.logger import log
from sync.models import Source, Media, MediaServer
from sync.signals import media_post_delete
from sync.tasks import rescan_media_server
class Command(BaseCommand):
help = ('Deletes a source by UUID')
def add_arguments(self, parser):
parser.add_argument('--source', action='store', required=True, help='Source UUID')
def handle(self, *args, **options):
source_uuid_str = options.get('source', '')
try:
source_uuid = uuid.UUID(source_uuid_str)
except Exception as e:
raise CommandError(f'Failed to parse source UUID: {e}')
log.info(f'Deleting source with UUID: {source_uuid}')
# Fetch the source by UUID
try:
source = Source.objects.get(uuid=source_uuid)
except Source.DoesNotExist:
raise CommandError(f'Source does not exist with '
f'UUID: {source_uuid}')
# Detach post-delete signal for Media so we don't spam media servers
signals.post_delete.disconnect(media_post_delete, sender=Media)
# Delete the source, triggering pre-delete signals for each media item
log.info(f'Found source with UUID "{source.uuid}" with name '
f'"{source.name}" and deleting it, this may take some time!')
source.delete()
# Update any media servers
for mediaserver in MediaServer.objects.all():
log.info(f'Scheduling media server updates')
verbose_name = _('Request media server rescan for "{}"')
rescan_media_server(
str(mediaserver.pk),
priority=0,
verbose_name=verbose_name.format(mediaserver),
remove_existing_tasks=True
)
# Re-attach signals
signals.post_delete.connect(media_post_delete, sender=Media)
# All done
log.info('Done')

View File

@ -0,0 +1,55 @@
import os
from pathlib import Path
from django.core.management.base import BaseCommand, CommandError
from common.logger import log
from sync.models import Source, Media
class Command(BaseCommand):
help = ('Scans download media directories for media not yet downloaded and ',
'marks them as downloaded')
extra_extensions = ['mp3', 'mp4', 'avi']
def handle(self, *args, **options):
log.info('Building directory to Source map...')
dirmap = {}
for s in Source.objects.all():
dirmap[s.directory_path] = s
log.info(f'Scanning sources...')
file_extensions = list(Source.EXTENSIONS) + self.extra_extensions
for sourceroot, source in dirmap.items():
media = list(Media.objects.filter(source=source, downloaded=False,
skip=False))
if not media:
log.info(f'Source "{source}" has no missing media')
continue
log.info(f'Scanning Source "{source}" directory for media to '
f'import: {sourceroot}, looking for {len(media)} '
f'undownloaded and unskipped items')
on_disk = []
for (root, dirs, files) in os.walk(sourceroot):
rootpath = Path(root)
for filename in files:
filepart, ext = os.path.splitext(filename)
if ext.startswith('.'):
ext = ext[1:]
ext = ext.strip().lower()
if ext not in file_extensions:
continue
on_disk.append(str(rootpath / filename))
filemap = {}
for item in media:
for filepath in on_disk:
if item.key in filepath:
# The unique item key is in the file name on disk, map it to
# the undownloaded media item
filemap[filepath] = item
continue
for filepath, item in filemap.items():
log.info(f'Matched on-disk file: {filepath} '
f'to media item: {item.source} / {item}')
item.media_file.name = filepath
item.downloaded = True
item.save()
log.info('Done')

View File

@ -0,0 +1,15 @@
import os
from django.core.management.base import BaseCommand, CommandError
from common.logger import log
from sync.models import Source, Media, MediaServer
class Command(BaseCommand):
help = ('Lists sources')
def handle(self, *args, **options):
log.info('Listing sources...')
for source in Source.objects.all():
log.info(f' - {source.uuid}: {source.name}')
log.info('Done')

View File

@ -0,0 +1,19 @@
from django.core.management.base import BaseCommand
from sync.models import Media
from common.logger import log
class Command(BaseCommand):
help = 'Resets all media item metadata'
def handle(self, *args, **options):
log.info('Resettings all media metadata...')
# Delete all metadata
Media.objects.update(metadata=None)
# Trigger the save signal on each media item
for item in Media.objects.all():
item.save()
log.info('Done')

View File

@ -0,0 +1,33 @@
from django.core.management.base import BaseCommand, CommandError
from django.utils.translation import gettext_lazy as _
from background_task.models import Task
from sync.models import Source
from sync.tasks import index_source_task
from common.logger import log
class Command(BaseCommand):
help = 'Resets all tasks'
def handle(self, *args, **options):
log.info('Resettings all tasks...')
# Delete all tasks
Task.objects.all().delete()
# Iter all tasks
for source in Source.objects.all():
# Recreate the initial indexing task
log.info(f'Resetting tasks for source: {source}')
verbose_name = _('Index media from source "{}"')
index_source_task(
str(source.pk),
repeat=source.index_schedule,
queue=str(source.pk),
priority=5,
verbose_name=verbose_name.format(source.name)
)
# This also chains down to call each Media objects .save() as well
source.save()
log.info('Done')

View File

@ -0,0 +1,34 @@
import os
from shutil import copyfile
from django.core.management.base import BaseCommand, CommandError
from django.db.models import Q
from common.logger import log
from sync.models import Source, Media
from sync.utils import write_text_file
class Command(BaseCommand):
help = 'Syncs missing metadata (such as nfo files) if source settings are updated'
def handle(self, *args, **options):
log.info('Syncing missing metadata...')
sources = Source.objects.filter(Q(copy_thumbnails=True) | Q(write_nfo=True))
for source in sources.order_by('name'):
log.info(f'Finding media for source: {source}')
for item in Media.objects.filter(source=source, downloaded=True):
log.info(f'Checking media for missing metadata: {source} / {item}')
thumbpath = item.thumbpath
if not thumbpath.is_file():
if item.thumb:
log.info(f'Copying missing thumbnail from: {item.thumb.path} '
f'to: {thumbpath}')
copyfile(item.thumb.path, thumbpath)
else:
log.error(f'Tried to copy missing thumbnail for {item} but '
f'the thumbnail has not been downloaded')
nfopath = item.nfopath
if not nfopath.is_file():
log.info(f'Writing missing NFO file: {nfopath}')
write_text_file(nfopath, item.nfoxml)
log.info('Done')

View File

@ -0,0 +1,20 @@
import json
from django.core.management.base import BaseCommand, CommandError
from sync.youtube import get_media_info
from common.utils import json_serial
class Command(BaseCommand):
help = 'Displays information obtained by youtube-dl in JSON to the console'
def add_arguments(self, parser):
parser.add_argument('url', type=str)
def handle(self, *args, **options):
url = options['url']
self.stdout.write(f'Showing information for URL: {url}')
info = get_media_info(url)
d = json.dumps(info, indent=4, sort_keys=True, default=json_serial)
self.stdout.write(d)
self.stdout.write('Done')

View File

@ -53,6 +53,8 @@ def get_best_audio_format(media):
# If the format has a video stream, skip it # If the format has a video stream, skip it
if fmt['vcodec'] is not None: if fmt['vcodec'] is not None:
continue continue
if not fmt['acodec']:
continue
audio_formats.append(fmt) audio_formats.append(fmt)
audio_formats = list(reversed(sorted(audio_formats, key=lambda k: k['abr']))) audio_formats = list(reversed(sorted(audio_formats, key=lambda k: k['abr'])))
if not audio_formats: if not audio_formats:
@ -66,7 +68,7 @@ def get_best_audio_format(media):
# No codecs matched # No codecs matched
if media.source.can_fallback: if media.source.can_fallback:
# Can fallback, find the next highest bitrate non-matching codec # Can fallback, find the next highest bitrate non-matching codec
return False, audio_formats[0] return False, audio_formats[0]['id']
else: else:
# Can't fallback # Can't fallback
return False, False return False, False
@ -88,6 +90,8 @@ def get_best_video_format(media):
# If the format has an audio stream, skip it # If the format has an audio stream, skip it
if fmt['acodec'] is not None: if fmt['acodec'] is not None:
continue continue
if not fmt['vcodec']:
continue
if media.source.source_resolution.strip().upper() == fmt['format']: if media.source.source_resolution.strip().upper() == fmt['format']:
video_formats.append(fmt) video_formats.append(fmt)
# Check we matched some streams # Check we matched some streams

View File

@ -44,7 +44,9 @@ class PlexMediaServer(MediaServer):
'<p>The <strong>libraries</strong> is a comma-separated list of Plex ' '<p>The <strong>libraries</strong> is a comma-separated list of Plex '
'library or section IDs, you can find out how to get your library or ' 'library or section IDs, you can find out how to get your library or '
'section IDs <a href="https://support.plex.tv/articles/201242707-plex-' 'section IDs <a href="https://support.plex.tv/articles/201242707-plex-'
'media-scanner-via-command-line/#toc-1" target="_blank">here</a>.</p>') 'media-scanner-via-command-line/#toc-1" target="_blank">here</a> or '
'<a href="https://www.plexopedia.com/plex-media-server/api/server/libraries/" '
'target="_blank">here</a></p>.')
def make_request(self, uri='/', params={}): def make_request(self, uri='/', params={}):
headers = {'User-Agent': 'TubeSync'} headers = {'User-Agent': 'TubeSync'}
@ -124,7 +126,7 @@ class PlexMediaServer(MediaServer):
# Seems we have a valid library sections page, get the library IDs # Seems we have a valid library sections page, get the library IDs
remote_libraries = {} remote_libraries = {}
try: try:
for parent in parsed_response.getiterator('MediaContainer'): for parent in parsed_response.iter('MediaContainer'):
for d in parent: for d in parent:
library_id = d.attrib['key'] library_id = d.attrib['key']
library_name = d.attrib['title'] library_name = d.attrib['title']

View File

@ -0,0 +1,18 @@
# Generated by Django 3.1.4 on 2020-12-19 03:12
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('sync', '0004_source_media_format'),
]
operations = [
migrations.AlterField(
model_name='source',
name='source_type',
field=models.CharField(choices=[('c', 'YouTube channel'), ('i', 'YouTube channel by ID'), ('p', 'YouTube playlist')], db_index=True, default='c', help_text='Source type', max_length=1, verbose_name='source type'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 3.1.4 on 2020-12-19 03:12
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('sync', '0005_auto_20201219_0312'),
]
operations = [
migrations.AddField(
model_name='source',
name='write_nfo',
field=models.BooleanField(default=False, help_text='Write an NFO file with the media, these may be detected and used by some media servers', verbose_name='write nfo'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 3.1.4 on 2020-12-19 06:45
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('sync', '0006_source_write_nfo'),
]
operations = [
migrations.AlterField(
model_name='source',
name='write_nfo',
field=models.BooleanField(default=False, help_text='Write an NFO file in XML with the media info, these may be detected and used by some media servers', verbose_name='write nfo'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 3.1.4 on 2020-12-19 06:59
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('sync', '0007_auto_20201219_0645'),
]
operations = [
migrations.AddField(
model_name='source',
name='download_cap',
field=models.IntegerField(choices=[(0, 'No cap'), (604800, '1 week (7 days)'), (2592000, '1 month (30 days)'), (7776000, '3 months (90 days)'), (15552000, '6 months (180 days)'), (31536000, '1 year (365 days)'), (63072000, '2 years (730 days)'), (94608000, '3 years (1095 days)'), (157680000, '5 years (1825 days)'), (315360000, '10 years (3650 days)')], default=0, help_text='Do not download media older than this capped date', verbose_name='download cap'),
),
]

View File

@ -0,0 +1,30 @@
# Generated by Django 3.1.6 on 2021-02-18 04:42
import django.core.files.storage
from django.db import migrations, models
import sync.models
class Migration(migrations.Migration):
dependencies = [
('sync', '0008_source_download_cap'),
]
operations = [
migrations.AddField(
model_name='source',
name='download_media',
field=models.BooleanField(default=True, help_text='Download media from this source, if not selected the source will only be indexed', verbose_name='download media'),
),
migrations.AlterField(
model_name='media',
name='media_file',
field=models.FileField(blank=True, help_text='Media file', max_length=200, null=True, storage=django.core.files.storage.FileSystemStorage(location='/home/meeb/Repos/github.com/meeb/tubesync/tubesync/downloads'), upload_to=sync.models.get_media_file_path, verbose_name='media file'),
),
migrations.AlterField(
model_name='source',
name='media_format',
field=models.CharField(default='{yyyymmdd}_{source}_{title}_{key}_{format}.{ext}', help_text='File format to use for saving files, detailed options at bottom of page.', max_length=200, verbose_name='media format'),
),
]

View File

@ -0,0 +1,30 @@
# Generated by Django 3.2.7 on 2021-09-24 05:54
import django.core.files.storage
from django.db import migrations, models
import sync.models
class Migration(migrations.Migration):
dependencies = [
('sync', '0009_auto_20210218_0442'),
]
operations = [
migrations.AlterField(
model_name='media',
name='media_file',
field=models.FileField(blank=True, help_text='Media file', max_length=255, null=True, storage=django.core.files.storage.FileSystemStorage(location='/home/meeb/Repos/github.com/meeb/tubesync/tubesync/downloads'), upload_to=sync.models.get_media_file_path, verbose_name='media file'),
),
migrations.AlterField(
model_name='source',
name='index_schedule',
field=models.IntegerField(choices=[(3600, 'Every hour'), (7200, 'Every 2 hours'), (10800, 'Every 3 hours'), (14400, 'Every 4 hours'), (18000, 'Every 5 hours'), (21600, 'Every 6 hours'), (43200, 'Every 12 hours'), (86400, 'Every 24 hours'), (259200, 'Every 3 days'), (604800, 'Every 7 days'), (0, 'Never')], db_index=True, default=86400, help_text='Schedule of how often to index the source for new media', verbose_name='index schedule'),
),
migrations.AlterField(
model_name='source',
name='media_format',
field=models.CharField(default='{yyyy_mm_dd}_{source}_{title}_{key}_{format}.{ext}', help_text='File format to use for saving files, detailed options at bottom of page.', max_length=200, verbose_name='media format'),
),
]

View File

@ -0,0 +1,21 @@
# Generated by Django 3.2.11 on 2022-02-01 16:54
import django.core.files.storage
from django.db import migrations, models
import sync.models
class Migration(migrations.Migration):
dependencies = [
('sync', '0010_auto_20210924_0554'),
]
operations = [
migrations.AddField(
model_name='source',
name='write_json',
field=models.BooleanField(
default=False, help_text='Write a JSON file with the media info, these may be detected and used by some media servers', verbose_name='write json'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 3.2.12 on 2022-04-06 06:19
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('sync', '0011_auto_20220201_1654'),
]
operations = [
migrations.AlterField(
model_name='media',
name='downloaded_format',
field=models.CharField(blank=True, help_text='Video format (resolution) of the downloaded media', max_length=30, null=True, verbose_name='downloaded format'),
),
]

View File

@ -0,0 +1,25 @@
# Generated by Django 3.2.12 on 2022-04-06 06:19
from django.conf import settings
from django.db import migrations, models
def fix_media_file(apps, schema_editor):
Media = apps.get_model('sync', 'Media')
for media in Media.objects.filter(downloaded=True):
download_dir = str(settings.DOWNLOAD_ROOT)
if media.media_file.name.startswith(download_dir):
media.media_file.name = media.media_file.name[len(download_dir) + 1:]
media.save()
class Migration(migrations.Migration):
dependencies = [
('sync', '0012_alter_media_downloaded_format'),
]
operations = [
migrations.RunPython(fix_media_file)
]

View File

@ -0,0 +1,21 @@
# Generated by Django 3.2.15 on 2022-12-28 20:33
import django.core.files.storage
from django.conf import settings
from django.db import migrations, models
import sync.models
class Migration(migrations.Migration):
dependencies = [
('sync', '0013_fix_elative_media_file'),
]
operations = [
migrations.AlterField(
model_name='media',
name='media_file',
field=models.FileField(blank=True, help_text='Media file', max_length=255, null=True, storage=django.core.files.storage.FileSystemStorage(base_url='/media-data/', location=str(settings.DOWNLOAD_ROOT)), upload_to=sync.models.get_media_file_path, verbose_name='media file'),
),
]

View File

@ -0,0 +1,23 @@
# Generated by Django 3.2.17 on 2023-02-13 06:03
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('sync', '0014_alter_media_media_file'),
]
operations = [
migrations.AddField(
model_name='media',
name='manual_skip',
field=models.BooleanField(db_index=True, default=False, help_text='Media marked as "skipped", won\' be downloaded', verbose_name='manual_skip'),
),
migrations.AlterField(
model_name='media',
name='skip',
field=models.BooleanField(db_index=True, default=False, help_text='INTERNAL FLAG - Media will be skipped and not downloaded', verbose_name='skip'),
),
]

View File

@ -0,0 +1,34 @@
# Generated by Django 3.2.18 on 2023-02-14 20:52
from django.db import migrations, models
import sync.models
class Migration(migrations.Migration):
dependencies = [
('sync', '0015_auto_20230213_0603'),
]
operations = [
migrations.AddField(
model_name='source',
name='embed_metadata',
field=models.BooleanField(default=False, help_text='Embed metadata from source into file', verbose_name='embed metadata'),
),
migrations.AddField(
model_name='source',
name='embed_thumbnail',
field=models.BooleanField(default=False, help_text='Embed thumbnail into the file', verbose_name='embed thumbnail'),
),
migrations.AddField(
model_name='source',
name='enable_sponsorblock',
field=models.BooleanField(default=True, help_text='Use SponsorBlock?', verbose_name='enable sponsorblock'),
),
migrations.AddField(
model_name='source',
name='sponsorblock_categories',
field=sync.models.CommaSepChoiceField(default='all', possible_choices=(('all', 'All'), ('sponsor', 'Sponsor'), ('intro', 'Intermission/Intro Animation'), ('outro', 'Endcards/Credits'), ('selfpromo', 'Unpaid/Self Promotion'), ('preview', 'Preview/Recap'), ('filler', 'Filler Tangent'), ('interaction', 'Interaction Reminder'), ('music_offtopic', 'Non-Music Section'))),
),
]

Some files were not shown because too many files have changed in this diff Show More