Compare commits

...

1247 Commits

Author SHA1 Message Date
simon
4a64748da8 bump TA_VERSION 2023-02-18 10:53:07 +07:00
simon
5d8dc76e7a New install startup fix, #build
Changed:
- Fixed not loading new default configs at expected time
- Better startup error handling
2023-02-18 09:57:11 +07:00
simon
4dfc9cef52 better TA_HOST error handling 2023-02-18 09:56:50 +07:00
simon
571c2ce3a4 ensure load_new_defaults for all manage.py commands, #434 2023-02-18 09:20:09 +07:00
simon
229555d97d fix raise commanderror when migration failed 2023-02-18 09:14:49 +07:00
simon
73da59ffbf switch to redis-stack-server 2023-02-17 22:49:07 +07:00
simon
c7a05a9ba3 a warning about zip file backups, use snapshots instead 2023-02-17 22:08:11 +07:00
simon
9e1316b543 yt-dlp uploader id fix, #build
Changed:
- Fix for yt-dlp parsing error [2023.02.17]
- Fix for various thumbnail extraction errors
- Fix download queue filter
- [API] Add filter subscribed channels
2023-02-17 20:58:19 +07:00
simon
72924fab66 bump yt-dlp, #433 2023-02-17 20:25:21 +07:00
simon
72f091b85e fix download filter for ignored videos, #401 2023-02-17 14:04:52 +07:00
simon
fdf5b2d802 clarify video page access from title, #432 2023-02-17 13:48:00 +07:00
simon
a34c9479f2 use channel from queue for download query filtering, #431 2023-02-17 12:21:00 +07:00
simon
162c05628c rename PendingInteract youtube_id kwargs 2023-02-17 10:26:10 +07:00
simon
1125c0c4bf handle malformed thumbnail download, #430 2023-02-17 09:17:04 +07:00
simon
0e5421a558 bump django 2023-02-17 08:37:38 +07:00
simon
5ec0636807 [API] add filter subscribed to channel list 2023-02-14 11:12:58 +07:00
simon
6641db3e7e skip empty comment, #429 2023-02-13 12:42:55 +07:00
simon
77c26134d1 add print for thumbnail fallback, #425 2023-02-13 12:15:12 +07:00
simon
ef685ecb42 handle PIL error empty thumbnail image, #425 2023-02-13 11:55:47 +07:00
simon
df1018e70c bump redis 2023-02-11 18:24:41 +07:00
simon
0658424b94 Merge branch 'master' into testing 2023-02-11 17:29:09 +07:00
p0358
d1b5644c63 refresh /progress more often (3000 ms => 500 ms) (#427) 2023-02-11 17:28:46 +07:00
lamusmaser
4936f2fdf2 412 fix vid_type with downloads (single and bulk) (#413)
* update TA_VERSION v0.3.2

* Update `single_download` task to request proper `vid_type`.

* Modify `single_download` to match bulk download.

* pr 413: undo TA_VERSION update

---------

Co-authored-by: simon <simobilleter@gmail.com>
2023-02-11 17:07:36 +07:00
simon
0fcd6c086b add kip log output for is_upcoming or is_live 2023-02-10 22:16:04 +07:00
simon
34c5b2613c add list of notable mentions 2023-02-05 15:49:43 +07:00
simon
6b246e7808 instructions for ES on custom port 2023-02-04 09:07:45 +07:00
simon
eda7528f7d Startup Rewrite, #build
Changed:
- Changed startup functions to manage.py commands
- Changed disable json backup, enable snapshots by default
- Changed use commited django migrations
- Added YT link to download queue title
2023-02-03 15:35:40 +07:00
simon
fa46b30e61 add unstable footer 2023-02-03 15:26:14 +07:00
Krzysztof Olszewski
2acaeb34db #409:Ability to visit source URL for preview from download queue (#422) 2023-02-03 15:17:16 +07:00
simon
f8b0977a7f fix linter 2023-02-03 14:57:57 +07:00
simon
4bab1a9815 bump django 2023-02-03 14:50:59 +07:00
simon
ce7adfee3a disable backup, enable snapshot 2023-02-03 14:50:41 +07:00
simon
a350d735be commit migrations 2023-02-02 14:19:06 +07:00
simon
3f86786ee8 reduce comments log output 2023-02-02 14:14:32 +07:00
simon
d6c124aae8 collectstatic if not in debug 2023-02-02 13:12:12 +07:00
simon
2d942fe9b5 load defaults for schedule builder at startup 2023-02-02 13:06:00 +07:00
simon
f18e2ce599 remove old startup class 2023-02-02 12:46:54 +07:00
simon
08350181f3 create ta_startup manage.py command 2023-02-02 12:43:02 +07:00
simon
114c7c2811 return new version in is_updated 2023-02-02 12:06:24 +07:00
simon
83057d9e54 return files clearded from clear_dl_cache 2023-02-02 10:46:59 +07:00
simon
d73a131616 extend timeout 2023-02-01 16:06:40 +07:00
simon
1b5a810520 finalize connection check manage.py command 2023-02-01 16:05:29 +07:00
simon
2f685859cf bump bs4 lib 2023-02-01 15:27:28 +07:00
simon
cacf6e43b8 add envcheck and basic connection check startup command 2023-01-28 08:37:58 +07:00
simon
991ab6a2bd bump requests lib 2023-01-27 20:13:33 +07:00
simon
188eb9fc41 update feature request desc 2023-01-27 09:57:10 +07:00
simon
4db3775eba manage new feature expectations 2023-01-27 09:55:11 +07:00
simon
57841ea1c0 clarify keyword filter roadmap item 2023-01-27 09:41:57 +07:00
simon
3602ec4560 update TA_VERSION v0.3.2 2023-01-16 11:38:34 +07:00
simon
afa865edb1 Fix startup and channel page, #build
Changed:
- Fixed startup schedule builder for new installations
- Fixed shorts and streams page sizes set to 0
2023-01-16 10:00:10 +07:00
simon
bf5a93dae0 update TA_VERSION unstable 2023-01-16 09:59:56 +07:00
simon
4d65d45a59 disable shorts or streams for channels, #405 2023-01-16 09:51:03 +07:00
simon
c75ef72e74 fix linter 2023-01-16 09:22:47 +07:00
simon
5d0689abba fix startup schedule builder for new installs, #408 2023-01-16 09:13:58 +07:00
simon
c0e36133a1 remove timeouts on ES connection 2023-01-14 13:21:50 +07:00
simon
89ac53083f fix startup lock race 2023-01-14 12:42:22 +07:00
simon
18acf22d2e update TA_VERSION 2023-01-14 09:35:09 +07:00
simon
98366f1051 update tubearchivist-es 2023-01-14 09:34:21 +07:00
simon
122b7f0d82 Merge branch 'docs' into testing 2023-01-14 09:33:21 +07:00
simon
d6a861118f better watched state log output 2023-01-14 08:51:10 +07:00
simon
37e173a5cc update es permission error message 2023-01-13 17:56:01 +07:00
simon
18a4484fa8 fix reindex logic when not using cookies 2023-01-13 15:19:11 +07:00
simon
fc04fdadd5 add note about other channel subpages 2023-01-12 23:20:37 +07:00
simon
96cb5ef9ed update docs for v0.3.1 2023-01-12 22:14:05 +07:00
simon
f665a1c7f5 fix unknown videos at startup 2023-01-12 20:46:41 +07:00
simon
092c467a2e default to videos vid_type in get_youtube_details 2023-01-12 16:49:45 +07:00
simon
1471fce2c3 Reindex shorts and streams, #build
Changed:
- Match exiting videos with new video type
- Randomize version check schedule per installation
2023-01-12 10:53:53 +07:00
simon
8db361cc88 channel fullscan to fix vid_type in refresh task 2023-01-11 23:06:02 +07:00
simon
00d7c33af6 randomize version_check schedule 2023-01-11 22:00:44 +07:00
simon
7d2bdc58ba bump libraries 2023-01-11 22:00:24 +07:00
simon
61b9110a5c Shorts and Streams, #build
Changed
- Added separate channel pages for streams and shorts
- Changed URL parser to take shorts urls
- Changed hiding empty channel tabs from UI
- Fixed playlist validator to only validate active playlists
- Fixed reindex task not triggering for channels and playlists
- [API] Add download now endpoint
2023-01-07 18:45:48 +07:00
simon
d033573bd6 hide empty shorts and streams from page 2023-01-07 18:12:09 +07:00
simon
7d6a879966 bump python base file 2023-01-07 18:08:35 +07:00
simon
9a526f93f4 fix reindex active_key 2023-01-07 15:53:32 +07:00
simon
5888e5442a set default streams and shorts channel size to 50 2023-01-07 15:51:58 +07:00
simon
d5885273ac detect any shorts before adding to queue 2023-01-07 15:41:11 +07:00
simon
db4798754a match streams type with was_live 2023-01-07 09:58:53 +07:00
simon
646bc1b12e filter by vid_type for add to download queue 2023-01-07 09:45:01 +07:00
simon
d9f73622a5 refactor UrlParser into own module, rename enum fields 2023-01-06 15:37:55 +07:00
simon
cd8b8aafc6 bump libraries 2023-01-06 13:47:17 +07:00
simon
e299308711 match VideoTypeEnum with yt channel paths 2023-01-06 10:11:52 +07:00
simon
eccad8968b add unknown field to VideoTypeEnum 2023-01-04 11:54:39 +07:00
simon
72e9234b2b fix playlist validator only validating active playlists 2023-01-04 08:24:18 +07:00
simon
eb812d2b0a add timeouts 2023-01-02 22:16:20 +07:00
simon
ce4288ff66 add is_short standalone helper function 2023-01-02 21:36:08 +07:00
simon
14d5a7ee1a fix progress bar and video-tags overlap 2023-01-02 21:27:49 +07:00
simon
0c8830793c add vid_type tag to queue thumb 2023-01-02 17:44:02 +07:00
simon
46229db64c [API] add download now endpoint, fix vid_type 2023-01-02 16:50:53 +07:00
simon
7941b0412b set vid_type for pending items in ta_download 2023-01-02 13:28:49 +07:00
simon
3db6077adf bump pillow 2023-01-02 13:28:07 +07:00
simon
d690c4591c Merge branch 'master' into testing 2023-01-02 10:11:53 +07:00
Chance Turner
98f5b66826 Lives and shorts (#395)
* Initial shot at adding in live and short videos

* Fix for typo on config variable names

* Remove was_live youtube flag

* Adding startup script to support setting vid_type on ta_video for videos created prior to support for live and shorts

* Linting updates

* More linting fixes

* Removing title variable from loop tuple unpacking
2023-01-02 10:11:06 +07:00
simon
43a41973d9 fix missing run method call 2022-12-31 16:06:33 +07:00
simon
00379bfe26 fix cookie validate bool logic 2022-12-31 15:35:58 +07:00
simon
7f603cb06a sponsorblock fix, #build
Changed:
- Fixes sponsorblock integration after remote API change
2022-12-31 11:42:10 +07:00
simon
34ed6ebc63 fix sponsorblock segments parser, #397 2022-12-31 11:17:11 +07:00
simon
14e0429758 API endpoints, #build
Changed:
- [API] Added delete video endpoint
- [API] Added delete channel endpoint
- [API] Added watched state endpoint
- Changed download queue interaction to existing endpoints
- Added update notification task
2022-12-23 23:01:28 +07:00
simon
73e1bba541 fix refactor codelist init 2022-12-23 23:00:35 +07:00
simon
599dd26b53 refactor comment interface into reusable CommentList class 2022-12-23 22:34:25 +07:00
simon
0b60377e19 cleanup, fix lint 2022-12-23 20:43:36 +07:00
simon
6ed2308f99 [API] add watched state endpoints 2022-12-23 20:39:14 +07:00
simon
a2e2fd1b89 add missing trailing / for endpoints docs 2022-12-23 11:08:30 +07:00
simon
09556dbf21 switch deleteQueue to API call 2022-12-22 19:57:19 +07:00
simon
470ecb007d switch re-add to queue to API call 2022-12-22 19:48:24 +07:00
simon
8af10f8c07 switch ignore to API call 2022-12-22 19:42:59 +07:00
simon
7a9f5e5685 [API] add and use DELETE channel endpoint 2022-12-22 19:24:48 +07:00
simon
bad4b48573 switch forget ignored to API call 2022-12-22 19:12:09 +07:00
simon
db21ee8bcb [API] add and use DELETE video endpoint 2022-12-22 19:01:13 +07:00
simon
63f35b19fe register version_check schedule 2022-12-22 11:17:06 +07:00
simon
ef44cb852c add new version notification to UI 2022-12-21 18:26:32 +07:00
simon
ae6bb4e757 consolidate min var initiate for simple views 2022-12-21 17:44:28 +07:00
simon
5244cddeb3 add version_check background task 2022-12-21 14:24:24 +07:00
simon
fff389359a update unstable TA_VERSION 2022-12-21 12:06:35 +07:00
simon
49961b2c4b add comment for min CPU requirements, #384 2022-12-19 16:07:23 +07:00
simon
c987c13fc7 manual reindex, #build
- Added reindex buttons for videos, channels, playlists
- [API] Added endpoints to controll reindex queue
- Added disctiction between *False* and *None* comments
- Fix for cast, enable by setting ENABLE_CAST=True env var
- Fix for failed comment extraction failing whole video
2022-12-19 15:37:38 +07:00
simon
4cccb73812 use timestamp() instead of strftime 2022-12-19 15:05:47 +07:00
simon
e804dd1aec continue indexing if comment indexing failes, #383 2022-12-19 14:59:07 +07:00
simon
f45a3095cb fix cast, use ENABLE_CAST env var to enable, #331 2022-12-19 14:43:10 +07:00
simon
3b4969dcea distinct between none and false comments 2022-12-19 14:12:48 +07:00
simon
18f6455eb2 add reindex buttons to templates 2022-12-19 13:04:53 +07:00
simon
71dd63d3f0 [API] implement refresh GET and POST endpoints 2022-12-19 11:24:37 +07:00
simon
21028d4e25 dict better REINDEX_CONFIG datatype 2022-12-12 18:21:31 +07:00
simon
2753ce93a2 add contains to RedisQueue 2022-12-12 18:01:02 +07:00
simon
3b3d151ec3 add reindex task lock, implement add to running queue 2022-12-12 17:33:57 +07:00
simon
c25af5cfaa add basic architecture overview 2022-12-12 17:10:47 +07:00
simon
018b578982 [API] add reindex endpoing 2022-12-11 17:13:07 +07:00
simon
0f6bc3a420 refactor check_reindex task for ReindexManual 2022-12-11 15:56:44 +07:00
simon
617790b68f add ReindexManual to control reindex from API 2022-12-11 15:39:40 +07:00
simon
762a0fe8a1 add default sort _doc in IndexPaginate 2022-12-11 14:57:39 +07:00
simon
0d21bfe929 refactor Reindex to use RedisQueue 2022-12-11 12:03:21 +07:00
simon
abc3150f59 bump libraries 2022-12-11 12:02:38 +07:00
simon
49b120ed2b refactor RedisQueue to take a queue_name arg 2022-12-10 14:51:17 +07:00
simon
37218fea1f add discrod tile 2022-12-10 14:32:39 +07:00
simon
2ad9448dfa update discord link to redirect 2022-12-03 09:37:31 +07:00
crocs
5c63d3cdc6 fixed discord links (#374) 2022-12-03 08:04:14 +07:00
simon
bf6cd01d80 hotfix: garantee new redis vars at startup, #372 2022-12-02 15:45:46 +07:00
simon
c06f6c68b4 fix empty comment reindex 2022-12-01 18:13:04 +07:00
crocs
706a710587 Update Installation.md (#373)
* Update Installation.md

updated Unraid install instructions
2022-12-01 10:36:07 +07:00
simon
31391c5289 update roadmap 2022-11-30 09:46:50 +07:00
simon
40c4ad1f30 add similar videos api endpoint docs 2022-11-30 09:23:21 +07:00
simon
41c530e994 update es version 2022-11-30 09:09:30 +07:00
simon
e98d2d3590 bump footer version 2022-11-30 08:52:45 +07:00
3ster
61ccb1c47a Fix example format specifier for iOS-compatible downloads (#370) 2022-11-30 08:50:56 +07:00
simon
b4b287bda4 fix spelling 2022-11-28 21:36:24 +07:00
simon
ca8f4c9fa4 update docs for comments and similar videos 2022-11-28 21:31:49 +07:00
simon
18b8a17df1 Use snapshots for mapping backup, #build
Changes:
- Use system snapshots at startup to backup before mapping changes
- Refactor startup, index setup and backup classes
- bump yt-dlp and redis
2022-11-27 18:26:45 +07:00
simon
f192c39be8 make snapshot before mapping changes 2022-11-27 18:01:57 +07:00
simon
f1b89345e2 refactor ElasticBackup and ElasticIndex for better startup handling 2022-11-27 15:41:59 +07:00
simon
f6c702fba7 refactor and run clear_dl_cache at startup 2022-11-25 18:49:36 +07:00
simon
f619a5f72d fi none existing unset channel overwrite, #362 2022-11-25 18:18:02 +07:00
simon
f7dd77ee3e bump redis 2022-11-25 15:58:43 +07:00
simon
0f41c54406 fix linebreak for comments header 2022-11-25 15:58:35 +07:00
simon
13d1927840 clean up API logging 2022-11-24 11:11:36 +07:00
simon
d0124c1a5c update yt-dlp, fix channel entries extractor 2022-11-22 19:11:58 +07:00
simon
5ed80d983d fix deactivated comment extractor 2022-11-22 18:01:27 +07:00
simon
24d66e33a7 video page improvements, #build
Changed:
- Added similar videos
- Added video tag cloud
- Added comment reply toggle
- Added comments progress
- Fixed channel and playlist deactivate
2022-11-22 11:43:44 +07:00
simon
786caf7bc1 Merge branch 'master' into testing 2022-11-22 11:36:52 +07:00
Gabe Dunn
e4b7258f7d Add podman installation instructions to docs (#367)
* add podman instructions to installation docs

* add mention of podman differences and docs page in readme installation section

* make mention of podman instructions more efficient
2022-11-22 11:36:09 +07:00
simon
90258e75e6 add notification for comment indexing 2022-11-22 11:23:34 +07:00
simon
b48b7c7e72 fix channel deactivation, take 2 2022-11-22 10:17:44 +07:00
simon
454d520394 standard video thumb path building 2022-11-21 15:50:01 +07:00
simon
ee5f83e89f add player-wrapper to video.html 2022-11-21 15:31:54 +07:00
simon
fa518c029c frontend similar videos 2022-11-21 15:13:37 +07:00
simon
aa1d0b759e [API] implement similar video endpoint 2022-11-21 15:13:15 +07:00
simon
abfd109203 hide comment box overflow 2022-11-21 15:05:28 +07:00
simon
ebf53f6486 fix channel deactivation 2022-11-21 14:55:47 +07:00
simon
2450a4ad1d future proof arm64 instructions 2022-11-21 13:46:19 +07:00
simon
41ea0c2c58 better padding for comment reply threads 2022-11-21 10:38:30 +07:00
simon
acb24e38a6 add tag cloud to video page, short rating 2022-11-21 10:27:28 +07:00
simon
a1fbc0b436 fix linter 2022-11-21 10:05:28 +07:00
simon
e052935e2d add comment reply toggle 2022-11-21 09:55:33 +07:00
simon
4d2bd51603 Merge branch 'master' into testing 2022-11-21 07:31:46 +07:00
simon
b3d3ebe9d0 fix channel_last_refresh mapping for date math 2022-11-20 20:38:10 +07:00
simon
d05c00aac6 fix playlist deactivate for refresh 2022-11-20 16:26:54 +07:00
simon
0c8fcbd724 use mapping to fix error for temp backup index 2022-11-20 16:13:30 +07:00
Simon
7e67ed02aa fix checkbox label 2022-11-20 15:45:55 +07:00
simon
e101c4abc7 ignore unavailable index for snapshots 2022-11-20 12:33:39 +07:00
simon
6a7a84792e better feedback when skipping not yet existing index backup 2022-11-20 12:21:25 +07:00
simon
2fa907c478 Index Comments, #build
Changed:
- added comment download and index
- [API] added comment api endpoints
2022-11-18 11:36:51 +07:00
simon
cc965ac473 update TA_VERSION to unstable 2022-11-18 11:35:09 +07:00
simon
8f5311ff04 delete comments when deleting channel 2022-11-18 11:33:06 +07:00
simon
e590d456a1 delete comments when deleting video 2022-11-18 10:47:48 +07:00
simon
a5be5a719b reindex comments 2022-11-18 10:31:09 +07:00
simon
b5ceb264ec format comment like number 2022-11-18 09:51:18 +07:00
simon
31c967cc11 fix linter 2022-11-18 09:46:33 +07:00
simon
2818bd5d52 add comments to video template if available 2022-11-18 09:44:20 +07:00
simon
939cd7fb62 index comments as a DownloadPostProcess 2022-11-18 09:22:58 +07:00
simon
172ced7129 configure comments extraction 2022-11-18 08:59:04 +07:00
simon
11c122cba3 finetune comments icon position 2022-11-18 08:58:25 +07:00
simon
456bc57df3 Merge branch 'master' into testing 2022-11-14 16:34:15 +07:00
crocs
091024553b Fixed "Unseen" and "Thumb" icons (#363)
* Update channel.html

Updated wording, removed "matching" as it's not necessary.

* Update downloads.html

* Delete icon-thumb.svg

* Delete icon-eye.svg

* Add files via upload
2022-11-14 16:33:26 +07:00
simon
ad016df105 implement comment frontend 2022-11-14 16:31:49 +07:00
simon
79d134cea8 clean format comments date string 2022-11-14 11:59:59 +07:00
simon
fb046bed5b [API] implement get comments API view 2022-11-12 12:42:08 +07:00
simon
f6b6185fb2 rename ta_comment index, implement get comments from es 2022-11-12 12:40:14 +07:00
simon
5b7e3e877b implement basic comment archive 2022-11-12 11:48:48 +07:00
simon
3c5302336f Merge branch 'master' into testing 2022-11-10 10:06:42 +07:00
simon
55c9721b51 rename to *common errors*, better issue template 2022-11-10 10:05:14 +07:00
simon
fc788c586d better wording for scheduler frequency, #358 2022-11-10 10:05:14 +07:00
simon
e912cf7fb6 rename to *common errors*, better issue template 2022-11-10 09:53:49 +07:00
lamusmaser
c111050c8f Update Synology documentation to include snapshot flag details. (#361) 2022-11-10 09:38:07 +07:00
simon
0e8d3b76c1 better wording for scheduler frequency, #358 2022-11-08 08:19:09 +07:00
simon
eda56236ce bump TA_VERSION 2022-11-05 11:58:41 +07:00
simon
b4620504b8 improve manual import docs 2022-11-05 11:36:53 +07:00
simon
b751ae183a fix startup migration error missing enable_snapshot var 2022-11-05 11:17:12 +07:00
simon
0b2b8aacef add snapshot wiki link 2022-11-05 09:35:28 +07:00
simon
e21d66ce85 add video player shortcuts docs 2022-11-03 18:18:51 +07:00
simon
66b72da02a update fuzzy search docs 2022-11-03 18:08:50 +07:00
simon
e0d6ce6c7f update snapshot docs 2022-11-03 17:58:17 +07:00
simon
97e9d6f8ce add path.repo env var to es container 2022-11-03 17:34:03 +07:00
crocs
0fb2f3fcc6 Changed the wording in a few places (#357)
* Update channel.html

Updated wording, removed "matching" as it's not necessary.

* Update downloads.html
2022-11-03 17:14:55 +07:00
simon
32fc89f4e7 configure fuzzines and snapshot, #build
Changed:
- Added *fuzzy:* secondary keyword for search
- Added take snapshot now button
- Changed snapshot policy to 12pm TZ aware
- Fixed channel page size validator > 0
2022-11-02 12:29:00 +07:00
simon
10fbfadf51 bump TA_VERSION unstable 2022-11-02 12:23:34 +07:00
simon
e9c457cac2 bump django 2022-11-02 12:18:17 +07:00
simon
0f191d8a3e configure fuzziness in search 2022-11-02 11:59:31 +07:00
simon
dcf317e471 form validate channel pagesize greater than 0, #334 2022-11-02 10:30:21 +07:00
simon
749261c146 take snapshot at 12 localtime 2022-11-02 10:17:27 +07:00
simon
238c6bc080 Merge branch 'master' into testing 2022-11-02 08:42:24 +07:00
simon
476d4d4909 add create snapshot now button 2022-10-31 21:21:28 +07:00
simon
651a642e71 bump pillow 2022-10-29 21:33:16 +07:00
simon
01a8c65df9 move version_check.py to separate arg 2022-10-29 21:21:22 +07:00
simon
d69460bf98 System snapshots, #build
Changed:
- Added: Dedublicated snapshots, read docs/Settings#snapshots first
- Added: Actions for better Python and JS linting
- Changed: Clean up JS
- Changed: Use patched ffmpeg builds for arm64
- API: Added endpoints to interact with snapshots
- Fixed: mobile layout for channel filter dopdown on downloads
2022-10-29 21:10:04 +07:00
lamusmaser
f0848283bd README updated with format update (#353)
* Add update to README to include format adjustments under Getting Started.

* Include updates to the Settings page of the Wiki.
2022-10-29 21:04:49 +07:00
simon
fdc1df313b add partial snapshots docs 2022-10-29 21:03:27 +07:00
simon
b425e7d75c frontend API button to restore snapshot 2022-10-29 20:12:54 +07:00
simon
fb25b76950 fix spelling 2022-10-29 17:45:20 +07:00
simon
a18723991a implement snapshot API views 2022-10-29 17:43:38 +07:00
simon
74aa134971 fix next_exec epoch time builder 2022-10-29 17:43:14 +07:00
simon
24b02150a7 handle delete, restore snapshot return values 2022-10-29 17:27:04 +07:00
simon
f717c71c66 implement _parse_single_snapshot, slug compatible naming 2022-10-29 16:54:16 +07:00
simon
814de87768 add snapshot check at startup 2022-10-29 15:57:59 +07:00
simon
c392563ef8 use exist_ok for os.makedirs at startup 2022-10-29 15:52:33 +07:00
simon
1fbd603374 frontend implementation for snapshot integration 2022-10-29 15:49:47 +07:00
simon
749f61aba6 fix empty policy building 2022-10-29 15:32:08 +07:00
simon
59f347c135 implement snapshot startup check 2022-10-28 16:47:50 +07:00
simon
904f449d37 combine snapshot_stats for frontend 2022-10-27 18:40:27 +07:00
simon
9b69a8dc91 fix mobile view-controls break 2022-10-27 16:15:32 +07:00
Kevin Gibbons
50ea7db66c Add FAQ item about downloading vs subscribing (#352)
* Add FAQ item about downloading vs subscribing

* note download+subscribe
2022-10-27 13:44:16 +07:00
lamusmaser
e4c5529412 Update Github Actions to use deploy.sh and remove individual calls for testing functions. (#351)
* Remove individual calls for testing functions.
Call `deploy.sh validate` for linting checks.

* Increment actions versions to remove deprecation errors.
Add `requests` to `pip install` line.

* Specify Python version with `actions/setup-python` call.
2022-10-27 13:30:24 +07:00
Kevin Gibbons
b4fe9a784f add github workflow to enforce linting of JS files (#348) 2022-10-27 13:27:30 +07:00
BrianCArnold
6b2fe125d4 Add LDAP attribute mapping env variables. (#344)
* Added ability to specify LDAP attribute mapping.

Specifically made sure that LDAP auth works the same if the
new environment variables aren't set, in order to maintain
behavior for users who are already using LDAP if they don't
set the new envvars.

* Updated env var name to match the name of the parent global.

* Updated README.md to include information on new user attribute mapping environment variables.

* Added additional environment var options, and updated comment explaining why there are multiple.

* I'm not a python programmer, so these were stupid mistakes. Works now.

* Addressing lint error.

* Finished updating formatting according to black linter.

* Shortened comments to fit within line length.

* fix whitespace linting

Co-authored-by: simon <simobilleter@gmail.com>
2022-10-27 13:20:41 +07:00
simon
e7f960bf46 implement basics snapshot management 2022-10-26 23:17:55 +07:00
simon
02ed521f21 bump uwsgi 2022-10-26 23:17:03 +07:00
Kevin Gibbons
39902cb1c6 Format/slightly modernize the JS (#345)
* add basic JS tooling

* fix accidental uses of global variables

* auto-format

* add and fix a couple more standard lint rules

* remove useless return false from settimeout callbacks

* document JS contributing

* fix whitespace in package.json

* add JS stuff to codespell skiplist

* codespell take two

* update github action and add comments about duplicated logic
2022-10-26 09:43:58 +07:00
simon
700a8cb54a remove leftover ffmpeg binary 2022-10-25 12:13:11 +07:00
simon
3eacc06383 Player shortcuts, #build
Changed:
- Added player shortcuts, '?'
- Fixed playlist missing channel metadata
- Changed arm64 build to use patched ffmpeg
2022-10-25 11:25:12 +07:00
simon
f8dccfcd15 use linuxarm64 ffmpeg builds for arm64 2022-10-25 11:22:32 +07:00
simon
725f17bcd2 fix playlist missing channel metadata 2022-10-25 11:13:14 +07:00
Kevin Gibbons
ff82690d3c add keyboard shortcuts to video player (#342)
* add keyboard shortcuts to video player

* fix modal on the inline player
2022-10-24 20:11:00 +07:00
Kevin Gibbons
51f7210195 More detailed contributing steps (#343)
* More detailed contributing steps

* restore whitespace
2022-10-24 19:59:29 +07:00
Kevin Gibbons
25d2ddd08a More detailed install instructions (#340) 2022-10-24 10:38:52 +07:00
simon
4af12aee15 better indentation formatting 2022-10-23 14:14:30 +07:00
simon
ed341fbc51 add cast auth comment to wiki 2022-10-23 14:11:40 +07:00
simon
f0874b2d02 add timeout for sponsorblock api requests, handle 503 2022-10-23 12:46:10 +07:00
simon
baacd3ee39 better message for running and indexing queue 2022-10-23 12:21:27 +07:00
simon
9cd23c3666 error handeling for playlist_thumbnail extraction 2022-10-23 11:50:26 +07:00
simon
0e17e2a6cf bump TA_VERSION to v0.2.3 2022-10-23 10:55:57 +07:00
simon
2dea0aa57b bump archivist-es 2022-10-23 10:55:14 +07:00
simon
ba1c8c15c4 fix missing build-arg for local deployment 2022-10-23 10:50:16 +07:00
simon
f16915be11 clarify filter only shows when more than 1 2022-10-23 10:49:26 +07:00
simon
858d437f3f simplify local production deployment 2022-10-23 00:11:03 +07:00
simon
de30ac302a add documentation for download channel filter 2022-10-23 00:10:38 +07:00
simon
dd8597307c fix wrong python version in final image 2022-10-22 23:30:44 +07:00
simon
608403c113 Download filter select, #build
Changed:
- added download filter dropdown
- fix for UnidentifiedImageError thumbnail error
- fix for _update_by_query channel missing watched_date
- fix for chrome compatibility text reveal
2022-10-22 22:55:38 +07:00
simon
40eff8e30e fix chrome compatibility issue for description text reveal, #327 2022-10-22 22:32:57 +07:00
simon
0bba36cbc3 add watched_date for _update_by_query mark as watched, #309 2022-10-22 22:00:35 +07:00
simon
a5788117de add dropdown channel agg for download page 2022-10-22 21:23:57 +07:00
simon
3f1075d0b2 bump python version 2022-10-22 20:24:58 +07:00
simon
dea2688b49 handle UnidentifiedImageError in ThumbManager, #325 2022-10-17 19:26:01 +07:00
simon
4f1daeb18c Downloads channel filter, #build
Changed:
- Added downloads channel filter to channel pages
- API: Filter download list view by channel
- Fixed: is_live status check
2022-10-17 19:01:59 +07:00
simon
927e6fa909 create channel parameter for downloads api view 2022-10-17 18:58:21 +07:00
simon
bd7cdb3942 append query parameters to pagination 2022-10-17 18:40:20 +07:00
simon
9dfd967a32 implement downloads filter per channel 2022-10-17 13:29:21 +07:00
simon
6eee762d3a add status for sub refresh 2022-10-14 11:27:27 +07:00
simon
a8c5773f81 fix is_live status check before adding to queue 2022-10-05 16:12:58 +02:00
simon
fbb52dc93f implement basic channel query string for downloads page 2022-10-05 15:47:17 +02:00
simon
c9e936da21 bump libraries 2022-10-05 15:46:44 +02:00
simon
c825e67f69 bump django and restframework 2022-09-30 18:02:44 +02:00
simon
bcb7b9443b bump tubearchivist-es 2022-09-19 15:52:22 +07:00
simon
896d166dcf add minimal system requirements documentation 2022-09-19 15:40:51 +07:00
simon
db6840639a update TA_VERSION to v0.2.2 2022-09-19 15:21:55 +07:00
simon
6af080ab6e remove unstable mention for TA_LDAP_DISABLE_CERT_CHECK 2022-09-19 15:14:22 +07:00
simon
f28c47323f cleanup channel info.json file after import 2022-09-19 15:10:41 +07:00
simon
73cd9a11f8 Consolidate downloads page UI, #build
Changed:
- use video styling from homepage also on download page
- configure grid items on download page
- fix reindex task trying to reindexing inactive videos
- better error handling with yt-dlp, timeout and retry
2022-09-17 11:32:31 +07:00
simon
19fed77218 bump python version 2022-09-17 10:53:43 +07:00
simon
b25818dbc3 extend timeout, add retry to YtWrap 2022-09-17 10:31:51 +07:00
simon
f2a8b070f2 add log output for add to queue 2022-09-17 10:30:27 +07:00
simon
dfacc3039a fix reindex task, skip inactive in _get_unrated_vids 2022-09-14 16:43:48 +07:00
simon
2b8ec1f8cf use grid_items for videos on download page 2022-09-13 16:47:31 +07:00
simon
265a53ace3 manual import fix, #build
Changed:
- fixed manual import errors, #311 #312
- fixed channel extraction to catch all alerts
- add LDAP disable cert check
- downgrade django, fix for DJANGO_DEBUG env var
2022-09-10 11:38:30 +07:00
simon
6d22e62642 downgrade django to fix django_debug env 2022-09-10 11:22:49 +07:00
simon
17c178c8bf add 400 response for wrong TA_HOST env var 2022-09-06 23:27:05 +07:00
simon
301a734905 fix DJANGO_DEBUG set to default empty string 2022-09-06 23:26:17 +07:00
simon
1c3febc49d force create thumb folder for manual import 2022-09-06 23:13:29 +07:00
simon
a6dd927860 fix channel extraction to catch all alerts, #312 2022-09-06 23:12:49 +07:00
simon
3c92de8e24 improved logging for manual import 2022-09-06 22:42:55 +07:00
simon
e51a662da9 fix manual import splitext matching invalid extensions, #311 2022-09-06 22:27:18 +07:00
simon
a7aead728c add roadmap link to FR template 2022-09-06 21:22:30 +07:00
simon
b2bf4ecc02 bump libs 2022-09-06 21:20:29 +07:00
simon
06795a014c explicitly set DJANGO_DEBUG env var on startup 2022-09-06 21:11:18 +07:00
simon
8f2794a141 Merge branch 'master' into testing 2022-09-03 15:17:06 +07:00
DanielBatteryStapler
396b6b80cd Add TA_LDAP_DISABLE_CERT_CHECK option (#314)
* add TA_LDAP_DISABLE_CERT_CHECK option

* add unstable disclaimer next to TA_LDAP_DISABLE_CERT_CHECK
2022-09-03 15:16:09 +07:00
simon
e467beb1c7 fix manual import cleanup metadata, #331 2022-08-26 17:07:55 +07:00
simon
c70942ff90 update old buildx command for reference 2022-08-20 15:26:28 +07:00
simon
f26d02919e revert faulty django update 2022-08-20 14:14:14 +07:00
simon
e65ba08ec6 untested offline import note 2022-08-20 13:42:31 +07:00
simon
09b75211c3 update roadmap 2022-08-20 08:51:29 +07:00
simon
6c019ba112 update installation wiki links and pages 2022-08-20 07:30:19 +07:00
simon
ac79ce91da verbose curl ES connection fail output 2022-08-19 18:51:16 +07:00
simon
20ff465b94 bump TA_VERSION to v0.2.1 2022-08-19 18:28:34 +07:00
simon
7d3a5cc699 add search api to readme 2022-08-19 18:26:27 +07:00
simon
93dc38e829 bump tubearchivist-es to 8.3.3 2022-08-19 18:20:58 +07:00
simon
17ddc95eb9 remove unstable mention from LDAP instructions 2022-08-19 18:20:25 +07:00
simon
1b22e61384 New search API endpoint, #build
Changed:
- Switched search form to API
- Additional validations for RYD
- Additional log output for curl ES startup check
2022-08-19 18:00:35 +07:00
simon
e6b673832f bump yt-dlp 2022-08-19 17:53:18 +07:00
PrivateGER
57a9fff82b Add API endpoint for searching (#303)
* Add API endpoint for searching + use it in frontend

* Fix linting warnings

* Remove multisearch API call

* Avoid 301 and fix up multiline comment
2022-08-19 17:52:37 +07:00
simon
1896e982b9 codespell lint fixes 2022-08-18 18:51:11 +07:00
simon
0e7e0a0560 fail open on ryd error 2022-08-18 18:45:46 +07:00
simon
115db944c8 add curl output for failed es connection 2022-08-18 18:35:09 +07:00
simon
650ac94e68 better wording for es permission error 2022-08-17 18:48:34 +07:00
simon
155439e228 add api Table of contents 2022-08-17 17:29:04 +07:00
simon
25877cf016 Lots of bug fixes, #build
Changed:
- fixed lots of small bugs
- fixed offline import mkv thumb extractor
2022-08-16 16:35:59 +07:00
lamusmaser
15996ea959 Update Installation documentation to include Synology Docker… (#282)
* Update Installation documentation to include Synology Docker instructions.

* Typo fix and adjustments.
2022-08-16 16:34:45 +07:00
simon
1a0421a45a update Manual Media Files Import docs 2022-08-16 16:29:17 +07:00
simon
0210a97b48 archive vtt files for offline import 2022-08-16 15:42:54 +07:00
simon
a4932b163b bump ryd 2022-08-16 15:42:26 +07:00
simon
0b41d288db fix mkv cover stream extractor 2022-08-16 12:19:27 +07:00
simon
21f3ee88b9 extract vtt from mkv and mp4 for import 2022-08-15 15:18:38 +07:00
simon
7727f533ee move subtitle to separate module 2022-08-15 13:55:24 +07:00
simon
41c71fde56 bump yt-dlp 2022-08-15 13:55:02 +07:00
simon
c27e446ec6 Merge branch 'master' into testing 2022-08-14 21:56:11 +07:00
simon
3d1561de75 better wording feature request help 2022-08-14 21:55:23 +07:00
simon
3df6eceb20 add issue templates 2022-08-14 19:43:00 +07:00
simon
81aa27e802 fix video-item.list vertical positioning 2022-08-13 16:20:38 +07:00
simon
bd4710ebdc improved wording for download form, subscription size, #300 2022-08-13 15:43:23 +07:00
simon
8a1ae1ef83 add protocoll to CSRF_TRUSTED_ORIGINS 2022-08-13 14:03:17 +07:00
simon
6afe25a4af fix empty subtitle download and index, #288 2022-08-12 15:53:20 +07:00
simon
cc40f46325 improved ryd error handeling, #283 2022-08-12 14:39:22 +07:00
simon
7c3bfa94f3 handle thumbnail download base exception, #281 2022-08-12 12:58:39 +07:00
simon
25f1539816 implement backup task lock, #278 2022-08-12 12:03:09 +07:00
simon
474ab22792 handle channel name redirect in UrlListParser, #276 2022-08-12 11:53:31 +07:00
simon
257f7a0287 add GH sponsor link 2022-08-12 06:19:36 +07:00
simon
c39ce61b2c offline import, #build
Changed:
- added offline import for videos and channels
- thumbnail manager rewrite
2022-08-12 05:36:54 +07:00
simon
980b99783b add sponsor info 2022-08-12 05:35:47 +07:00
simon
ececc3cedd bump yt-dlp 2022-08-11 19:57:12 +07:00
simon
0891c49620 implement offline channel import 2022-08-11 19:57:00 +07:00
simon
c26ef012c1 raise ValueError when not matching media files 2022-08-11 11:10:58 +07:00
simon
31061c0eb0 handle offline import without info.json fail 2022-08-10 22:38:17 +07:00
simon
994f7c2443 use user provided thumb for offline import 2022-08-10 22:31:15 +07:00
simon
7029441f89 use fallback thumb when no url passed 2022-08-10 22:24:02 +07:00
simon
35f1084cc2 download thumbnail for manual import 2022-08-10 21:47:26 +07:00
simon
44473a364f fix double download of playlist art 2022-08-10 21:20:58 +07:00
simon
fb4d6b7be3 major refactor ThumbManager 2022-08-10 21:03:54 +07:00
simon
b76fa69396 WIP: rewrite ManualImport, identify and archive 2022-08-08 19:18:27 +07:00
simon
8f711d359b implement youtube_meta and media_path overwritein YoutubeVideo 2022-08-08 18:28:32 +07:00
simon
9fd6f9c58b use absolute paths for manual import 2022-08-08 15:57:42 +07:00
simon
b70cbfa0a8 convert thumbnail to jpg for manual import 2022-08-08 14:52:33 +07:00
simon
d0675368f4 bump django 2022-08-08 14:52:06 +07:00
simon
195acd3246 WPI: dump thumbnails from mp4 and mkv 2022-07-31 00:02:09 +07:00
simon
5284975aa2 merge master into testing 2022-07-30 21:38:00 +07:00
simon
13018192f3 remove default funding sources 2022-07-30 21:17:12 +07:00
simon
07f50b6619 LDAP support, #build
Changed:
- configure LDAP over environment variables
- fix reverse proxy CSRF error
2022-07-30 21:13:13 +07:00
simon
36d0f08efb add FUNDING.yml 2022-07-30 21:08:57 +07:00
DanielBatteryStapler
fd75def6c3 implement LDAP as authentication backend support (#274) 2022-07-30 21:08:12 +07:00
simon
61da0c8af3 WIP: extract thumb from mkv 2022-07-30 21:07:31 +07:00
simon
91a169fa3e add FUNDING.yml 2022-07-30 21:07:31 +07:00
DanielBatteryStapler
e1c4702398 implement LDAP as authentication backend support (#274) 2022-07-30 21:05:10 +07:00
Aron Brown
5407d35779 Update settings.py (#284)
Add CSRF_TRUSTED_ORIGINS Django config settings and set it to environment variable TA_HOST.
This should fix login issues behind reverse proxies.
2022-07-30 21:03:58 +07:00
simon
26cc7846c6 WIP: new import folder parser for offline import 2022-07-26 19:51:47 +07:00
simon
36560735f2 better wording what TA_HOST represents, #275 2022-07-23 21:58:06 +07:00
simon
41972f64ad update roadmap 2022-07-23 19:57:25 +07:00
simon
83866a0d79 clarify scope 2022-07-23 17:32:59 +07:00
simon
aa0739bfbe bump TA_VERSION to v0.2.0 2022-07-23 16:35:18 +07:00
simon
d797f47bb8 remove synology link, remove security notification 2022-07-23 15:03:41 +07:00
simon
09f3ce0109 add TA_HOST and ES8 documentation 2022-07-23 14:38:50 +07:00
simon
2ba31a3937 add search help text 2022-07-23 14:10:16 +07:00
simon
31e2fcb322 update backup section for v0.2 2022-07-23 13:41:24 +07:00
simon
e69cd1129c add result size info 2022-07-23 13:41:07 +07:00
simon
8a59b66a0f finetune full text thumb width 2022-07-23 13:28:49 +07:00
simon
dcf7658972 add kibana quick start guide 2022-07-22 18:26:12 +07:00
simon
e79d64d707 update channel wiki page for v0.2 2022-07-22 18:15:27 +07:00
simon
503a4ed822 force es8 at startup 2022-07-22 17:22:06 +07:00
simon
f0a44678ed unset video max-height max-width for fullscreen webkit, #264 2022-07-22 14:50:53 +07:00
simon
4b1ebf44c9 handle filenotfounderror for manual cookie import 2022-07-21 23:01:32 +07:00
simon
eb229440d4 load config into redis on fresh install at startup 2022-07-21 23:01:01 +07:00
simon
88b412a2e9 keyword search, #build
Changed:
- added keyword search queries
- added fulltext search
- authenticate all user created media
- reduce docker log output
2022-07-21 20:15:15 +07:00
simon
14d3a7586d validate es8 at startup 2022-07-21 20:09:06 +07:00
simon
6ec65f9fe8 add search wiki page 2022-07-21 20:00:12 +07:00
simon
0ec6558282 jump to timestamp from full search 2022-07-21 17:15:36 +07:00
simon
52d31f9cca simplify createVideo player link building 2022-07-21 15:51:33 +07:00
simon
07016f2cc5 ignore full: result without highlight 2022-07-21 15:47:29 +07:00
simon
55a73995c8 implement source and lang keywords for full text search 2022-07-21 15:39:27 +07:00
simon
a40ebc7bbc tweak with for video-item.list 2022-07-21 15:36:36 +07:00
simon
e4dd47aed0 implement fulltext search keyword 2022-07-20 21:49:32 +07:00
simon
eb5a7338ab search with and operator, increase result size 2022-07-20 18:25:39 +07:00
simon
b77d50d4ed use queryType in populateMultiSearchResults to hide irrelevant 2022-07-20 18:01:57 +07:00
simon
b520a9bfb0 boost title result scores in multi_match 2022-07-20 16:20:23 +07:00
simon
242ca19586 return query_type from SearchParser 2022-07-20 16:10:57 +07:00
simon
6a6665bdf9 implement simple, aka none keyword query 2022-07-20 15:04:10 +07:00
simon
8e8c00af40 implement playlist keyword search, fix channel subscribed keyword 2022-07-20 14:58:04 +07:00
simon
ae3cf7eb80 add channel keyword search 2022-07-20 14:48:41 +07:00
simon
0a2b6ee90a add keyword search framework, implement video search 2022-07-19 19:23:59 +07:00
simon
d065b5b90a reduce uwsgi logging to errors only 2022-07-18 17:17:20 +07:00
simon
0528220b5e authenticate media and thumb requests 2022-07-18 14:10:34 +07:00
simon
f0850522a7 Merge branch 'master' into testing 2022-07-18 11:49:42 +07:00
simon
cc2fff8bef use info_dict for message, simplify cache template, #271 2022-07-18 11:37:43 +07:00
simon
2d13011866 bump yt-dlp 2022-07-18 11:29:28 +07:00
simon
146f17bf74 shutil.move fix take 2, #build
Changed:
- use shutil.copyfile for copy_function
- fixed typo
2022-07-17 07:48:54 +07:00
simon
b406250ae2 use shutil.copyfile for shutil.move calls 2022-07-17 07:47:27 +07:00
simon
ed9237b69d fix typo 2022-07-17 07:46:52 +07:00
simon
393e00866b Desc preview, nginx fix, #build
Changed:
- Requires new env var: TA_HOST, hostname of your application
- Showing preview for description text, by @p0358
- Fix nginx user permission, by @p0358
- Refactor IndexPaginate to take loop callback
- Fix backup for large index using loop callback
- Fix deactivating missing channels on reindex
2022-07-16 22:11:29 +07:00
p0358
411b09629a Better description view for videos and playlists (#272)
* draft

* proper implementation of description line clamping

* use a JS event that's potentially faster for text expand button visibility update

* remove desc title text, move show button after desc

Co-authored-by: simon <simobilleter@gmail.com>
2022-07-16 22:06:26 +07:00
p0358
79996f6838 Fix permission/mount issues from #268 (#270)
* make nginx run as root within the container (#268)

* move file to videos path with fallback to regular copy with no metadata copying (#268)

* use `copy_function=shutil.copy` everywhere with `shutil.move`
2022-07-16 21:54:39 +07:00
simon
6bb7f80ea2 Refactor IndexPaginate
- add callback function
- implement callback for ElasticBackup
2022-07-16 17:55:18 +07:00
simon
3b17c01c6d add TA_HOST env for ALLOWED_HOSTS 2022-07-14 17:26:10 +07:00
simon
91bccfd057 deactivate non existing channels on reindex 2022-07-12 11:41:03 +07:00
simon
8cca09e0ac remove ta_json backup file, keep only es_ndjson 2022-07-07 12:34:57 +07:00
simon
1479516689 Reimagining the channel pages, #build
Changed:
- Split the channel-id page into subpages
- Channel videos, channel playlist, channel about
- Disable es startup check to test ES8
2022-07-04 21:23:50 +07:00
simon
831e59a626 bump django 2022-07-04 21:23:32 +07:00
simon
6e012383ab toggle playlist subscribed filter 2022-07-04 21:12:54 +07:00
simon
e988192343 add channel_id_about subheading 2022-07-04 20:44:12 +07:00
simon
ef3abd01b1 implement channel_id_playlist, handle channel_id_about post 2022-07-04 20:25:52 +07:00
simon
e664338845 implement basic channel_id_about view 2022-07-04 16:44:37 +07:00
simon
63a1237683 temporarily disable es version check for testing 2022-07-04 16:43:13 +07:00
simon
6d59da4c86 remove redundant latest tags 2022-07-03 18:20:30 +07:00
simon
5bc67104ea set restart policy to unless-stopped 2022-07-03 17:23:02 +07:00
simon
72ec78e011 browser extension as prefered cookie sync method 2022-07-03 16:46:45 +07:00
simon
de8b7d6b43 bump ES version 2022-07-03 16:09:09 +07:00
simon
048409bb1e bump TA_VERSION to v0.1.7 2022-07-03 16:08:30 +07:00
simon
efff4cd96d change cookie documentation for v0.1.7 2022-07-03 16:06:28 +07:00
simon
78d7947a5c a note about subtitle refresh, #260 2022-07-03 15:36:21 +07:00
simon
c132a37f03 bump dependencies 2022-07-03 15:30:46 +07:00
simon
4678a7507b bump dependencies 2022-06-30 18:44:33 +07:00
simon
e8e240c89b Startup fix, cookie improvements, #build
Changed
- lock creating superuser at startup by @dshoreman
- auto validate cookie
- API: return cookie validation state for browser extension
- Finetuning CSS mobile layout
2022-06-27 10:14:59 +07:00
simon
1857ac44ca fix tablet css breakpoints and grid spacing 2022-06-27 09:48:36 +07:00
simon
8c07932071 delete cookie:valid key when revoking 2022-06-23 19:21:13 +07:00
simon
74d74d95a1 store validation result, return in API 2022-06-23 06:48:55 +07:00
simon
b4add20d10 bump yt-dlp 2022-06-22 19:30:38 +07:00
Dave Shoreman
e9619c1242 Fix repeated superuser creations after rename/removal (#263)
* Avoid creating superuser if variables are removed

Enables the removal of `TA_USERNAME` and/or `TA_PASSWORD` as a way of
preventing an additional superuser being created when another exists.

In the old code, `ELASTIC_USER` would never error due to the default, so
this replaces the loop with one requirement on `ELASTIC_PASSWORD`.

* Use .superuser_created to prevent re-creation

Creates an empty `.superuser_created` file after successful creation,
and uses this to skip the command on future runs. If the file doesn't
exist, then `TA_USERNAME` and `TA_PASSWORD` will also be required.

* The superuser password can't be empty

...well—it *can*—but the password input of the login form requires a
value so even though the account is created, you can't actually login.

* Parse createsuperuser output for 'exists' error

If a user already exists with the same name as the superuser being
created, the command fails with "Error: That name is already taken".

It's probably safe to assume that user is the one we expect. This parses
the output, treating it as successful so it can be skipped next time.

If it hits the catch-all, something probably failed so we exit.

* Use the cache for persistence

Updates the .superuser_created file to be placed in /cache so that it
gets persisted in a volume between config tweaks and container rebuilds.

Also makes `TA_PASSWORD` required always, because it seems to be needed
by tubearchivist/config/settings.py to generate a secret key.

* Let it work outside Docker

If /cache doesn't exist (i.e. when it's not running in Docker), this
falls back to a `cache` dir in the current working directory instead.

Also renames the file to 'initsu.lock' rather than having it hidden.

* Use flexible fallback path for `celery` command
2022-06-22 18:29:46 +07:00
simon
49d33a2765 Improved Cookie handeling, #build
Changed:
- Cookie: Use IO Stream from Redis directly for cookies
- Cookie: Don't load invalid cookie
- API: Import cookie with PUT request
- Redis: Don't expire Redis messages by default
- Redis: Connections expire automatically
2022-06-21 08:14:01 +07:00
simon
ecc58f6c11 API: note about release lagging behind 2022-06-21 08:10:00 +07:00
simon
e5f43e5fe9 API: implement cookie PUT request to import cookie 2022-06-21 08:06:38 +07:00
simon
499c47c7a1 move set_cookie to separate method for reusabliity 2022-06-21 08:01:33 +07:00
simon
2cf30e1127 refactor: default set_message in RedisArchivist to True 2022-06-16 10:37:46 +07:00
simon
ebfc4a349f center title and tiles 2022-06-15 18:30:06 +07:00
simon
150c1dc273 add tiles to readme 2022-06-15 18:18:52 +07:00
simon
30abbe9be7 rewrite cookie into redis from io stream, auto validate 2022-06-15 17:54:05 +07:00
simon
40e4ef0e05 auto expire redis connections after 3600secs 2022-06-15 16:36:41 +07:00
simon
2ceb1b701a consolidate config of cookie revoke 2022-06-15 15:39:24 +07:00
simon
2ad093a9a8 handle cookiejar.LoadError dont import invalid cookie 2022-06-15 10:39:48 +07:00
simon
09a94d0df5 set relative values directly with path arg 2022-06-15 10:27:29 +07:00
simon
f7f4ca7728 API update, #build
Changed:
- API: Added Task get view
- Added is_locked method for redis lock check
- Updaded base and libs
2022-06-15 09:24:01 +07:00
simon
83a90000c6 bump base image 2022-06-15 09:23:41 +07:00
lamusmaser
bbb16bb3c2 Update TaskAPIView with GET - #257 PR (#258)
* Initial commit for GET response for TaskAPIView.

* Update for missing space - linting issue.

* Additional linting fixes.

* add is_locked method to check if lock is set

* Update to use `.is_locked` method and add decorator.

* Fix linting issue.

* Fix doubled "rescan" call for locked file.
Removed call to ".owned()" under "is_locked".

* Commenting out GET call in TaskAPI view to revert change for testing.

* Commenting is_locked function to see if Server Response 500 is stopped.

* Reassert is_locked function within RedisArchivist class.

* Create test GET responder.

* Reverting simple GET response change.

* Reapplying simple GET response for `/api/task/`.

* Reapplying change with modification to lock key.

* Documentation update for new GET calls.

* README: fix returned value as `bool` instead of `str`.

* Updating the `is_locked` key reference to "rescan" and "downloading" to fix which key is being retrieved.

* Use dictionary keys, rather than line-by-line statements.

* Fix typo in README for Task View.

Co-authored-by: simon <simobilleter@gmail.com>
2022-06-15 09:15:59 +07:00
simon
4d9ee4494f bump requests 2022-06-14 14:26:49 +07:00
simon
eeeabca8c2 bump dependencies 2022-06-07 17:09:49 +07:00
simon
bb0f3dad1a update roadmap 2022-06-04 11:19:47 +07:00
simon
9c0c263fce new docker build for build server task 2022-06-04 11:16:46 +07:00
simon
aed2d34149 Merge branch 'testing' for release v0.1.6 2022-06-04 10:15:24 +07:00
simon
3504408e70 update TA_VERSION to v0.1.6 2022-06-04 09:47:51 +07:00
simon
41b3e06f73 update docs for v0.1.6 2022-06-01 17:00:06 +07:00
simon
c41f0a7cc4 try to LOAD_TRUNCATED_IMAGES, #256 2022-06-01 09:04:55 +07:00
simon
19d9af821d grid row ui improvements, #build
Changed:
- Better styled grid row, hid controlls if not applicable
- Changed subtitle naming convention
- Timeout for search as you type
- move delete queue button to settings page
2022-05-31 16:40:54 +07:00
simon
5d262d7d48 skip cookie if no config added to YtWrap 2022-05-30 18:22:17 +07:00
simon
231ac1e012 make channel matching optional for reusability 2022-05-30 17:59:52 +07:00
simon
cbb714c235 show grid count controls only if applicable 2022-05-30 16:21:24 +07:00
simon
c93e935b9c move delete download queue button to settings page 2022-05-30 16:06:12 +07:00
simon
c6d696a91b timeout search as you type 2022-05-30 15:29:52 +07:00
simon
a9b8d34b5a adapt search width with grid_items 2022-05-30 14:52:47 +07:00
simon
25e2bdb0b7 bump celery version 2022-05-30 14:51:44 +07:00
simon
bbb0c26498 fix subtitle delete overwrite, change filename to .lang.vtt, #195 2022-05-29 11:43:25 +07:00
simon
386c456415 variable gird row items, #build
Changed:
- implemented configurable video items in grid row
- fix subtitle parser bug with missing segs key
- fix also delete video from playlist
- fix channel video extractor after refactor
2022-05-28 21:54:47 +07:00
simon
51a45bb59e implement variable grid row size for playlist_id 2022-05-28 21:48:15 +07:00
simon
0759b4f220 reset mobile variable gird row count 2022-05-28 21:41:17 +07:00
simon
9f92bf95b4 mark video as downloaded false when deleting video 2022-05-28 21:23:56 +07:00
simon
e638f8b487 skip subtitle segment without segs key, #249 2022-05-28 17:47:21 +07:00
simon
caaed252f9 specify videos page for get_last_youtube_videos of channel 2022-05-28 14:34:01 +07:00
simon
5982932457 implement variable grid row size 2022-05-28 13:57:29 +07:00
simon
9a31243b1c log message when skipping video 2022-05-28 11:11:09 +07:00
simon
88e526f5e5 refactor new YtWrap class for yt-dlp calls, #build
Changed:
- new yt-dlp base class
- new cookie class using io_stream
- MetadataFromField postprocessors
2022-05-25 16:15:38 +07:00
Clark
730ac15349 Fix #241 Use title/uploader for embedded metadata not Content ID (#252) 2022-05-25 16:13:25 +07:00
simon
0ac9243389 remove unused monitor_cache_dir method 2022-05-24 15:54:41 +07:00
simon
c39f8a9cb8 refactor use cookie io_stream 2022-05-24 15:51:58 +07:00
simon
9dd1e9a29f extract playlists with new YtBase class 2022-05-24 10:44:18 +07:00
simon
3f5104d6d8 integrate new YtWrap class in download queue and subscriptions classes 2022-05-24 10:36:39 +07:00
simon
4b00f454b7 integrate new YtWrap class in yt_dlp handler 2022-05-24 10:19:02 +07:00
simon
33f27f018f handle download error 2022-05-24 10:18:33 +07:00
simon
a7d9b4584b implement new YtBase class for index classes 2022-05-24 10:01:30 +07:00
simon
4e699621a0 obs base and handle extract error 2022-05-24 10:00:40 +07:00
Heavybullets8
2dd51c38a7 Add Truenas Scale Instructions (#251)
included two <br /> to add spacing between the guides. 
If the website results in an ssl error at the time of submitting its because the new site is still in the process of being handed over to a different registrar, and its still getting its certs from github. Should be fixed by tomorrow.
2022-05-24 08:53:34 +07:00
simon
37e6f8656a yt-dlp base class 2022-05-24 08:53:05 +07:00
simon
1fd9981273 test new build server, #build
Changed:
- updated libraries
- trigger unstable build on new buildserver
2022-05-20 20:27:30 +07:00
simon
3a2a7dac40 update dev environment details 2022-05-20 20:26:31 +07:00
simon
ef69ab2799 test new build server, #build
Changed:
- updated libraries
- trigger unstable build on new buildserver
2022-05-20 20:10:18 +07:00
simon
f49e313e2f add dl speed schedule to roadmap 2022-05-20 20:09:06 +07:00
simon
9fe6d41320 update libraries 2022-05-20 20:05:31 +07:00
simon
c0ccc267c8 remove drone 2022-05-20 19:37:42 +07:00
simon
1aef0451b7 Merge branch 'master' into testing 2022-05-20 18:31:15 +07:00
simon
e8763b87cc fix manifest docker_token 2022-05-13 11:53:34 +07:00
simon
03527b4ace add manifest pipeline 2022-05-12 21:50:37 +07:00
simon
526d75af4f add arm64 build 2022-05-12 16:33:18 +07:00
simon
7e515b52e1 add TARGETPLATFORM build_args 2022-05-12 00:20:57 +07:00
simon
3679d28ffe use docker_token instead 2022-05-11 23:39:34 +07:00
simon
ced2452d6e image_pull_secrets with new creds 2022-05-11 22:42:30 +07:00
simon
b301a89ce1 change docker login 2022-05-11 22:29:04 +07:00
simon
0749cdd8f3 add TARGETPLATFORM env var 2022-05-11 21:59:15 +07:00
simon
da251b28a4 remove dry_run 2022-05-11 21:42:16 +07:00
simon
5bccbfbd84 explicitly set platform 2022-05-11 21:37:03 +07:00
simon
1f0ef10d35 test build to other account 2022-05-11 21:26:24 +07:00
simon
ab6689c7c8 test dockerconfigjson 2022-05-11 21:17:37 +07:00
simon
ddc3ea5e72 test drone pipeline 2022-05-11 19:15:47 +07:00
simon
669f1ffe2c dest drone pipeline 2022-05-11 18:44:57 +07:00
simon
03179eea94 bump redis 2022-05-10 16:32:18 +07:00
simon
e763c6d13e merge for v0.1.5 build 2022-05-08 08:25:10 +07:00
simon
14387eb8d6 update roadmap 2022-05-08 08:18:17 +07:00
simon
619370c670 add metrics to extended universe 2022-05-08 08:06:59 +07:00
simon
ca13ddec26 update es version 2022-05-08 08:06:37 +07:00
simon
0ab809447a update version 2022-05-07 19:06:43 +07:00
simon
f94bbec672 add cookie wiki link 2022-05-07 19:00:40 +07:00
simon
8cc6e77169 switch to compose v2 2022-05-07 18:33:16 +07:00
simon
5f63dc93ae add Extended Universe section to readme 2022-05-07 10:09:53 +07:00
simon
3722f11a65 cleanup cookie file after task 2022-05-07 09:34:51 +07:00
simon
087043811f avoid unneeded cookie open 2022-05-07 09:34:33 +07:00
simon
3ca86ba91d fix typo, improve wording, add ansible docker playbook 2022-05-06 10:17:23 +07:00
simon
93265a8dca implement cookie validation, #build
Changed:
- Added cookie validation button
- Fixed all the open bug reports! :-)
2022-05-06 08:13:26 +07:00
simon
160c23c36c conform import path style 2022-05-06 08:09:19 +07:00
simon
891911e56b note about cookie validation 2022-05-06 08:02:34 +07:00
simon
94fa191fb8 skip premium videos, clean exit for empty bulk_list, #237 2022-05-05 23:38:10 +07:00
simon
9a2b29c2ae implement channle name change in reindex, #211 2022-05-05 22:59:56 +07:00
simon
7a51368955 fix skip deleting none existing subtitles, add update_media_url 2022-05-05 22:56:22 +07:00
simon
7fa0d0f331 untangle delete subtitles from delete media file, #195 2022-05-05 09:11:37 +07:00
Nathan DeTar
4eba36f7a0 Cookie Validation Button (#242)
* Added cookie validate button & message placeholder

* Added handleCookieValidate(), postCookieValidate()

* show validation only if enbled, add processing message for immediate feedback

Co-authored-by: simon <simobilleter@gmail.com>
2022-05-04 17:07:01 +07:00
simon
6b3ff009da extend cookie functionality, #build
changes:
- integrate watch later and liked videos playlist
- merges sponsorblock state issue #236
- lots of bug fixes
2022-05-03 20:07:45 +07:00
simon
fcac10a6ce add cookie documentation 2022-05-03 18:17:36 +07:00
simon
1613b71942 hide cookie after rescan and download process 2022-05-03 17:32:32 +07:00
simon
83beb53860 simplify _add_single to call update_status in PendingInteract directly 2022-05-02 21:38:03 +07:00
simon
8a4c50779a standardize prints for PostData mapper 2022-05-02 18:39:54 +07:00
simon
ca5b00a373 refresh for PendingInteract delete to avoid race condition, #217 2022-05-02 18:20:56 +07:00
simon
34a1fe9e8e fix thumb re-embed task, #231 2022-05-02 17:38:20 +07:00
simon
34c18e75f5 add LL and WL as valid playlists with cookie 2022-05-02 11:30:38 +07:00
simon
6fbac12314 fix channel_validate in PlaylistSubscription, #223 2022-05-02 11:27:50 +07:00
Nathan DeTar
c4107fc370 Fix to prevent holding onto SB info. (#236) 2022-05-02 10:41:42 +07:00
simon
ca3ea20283 add socket_timeout when passing check_formats to yt-dlp 2022-05-02 09:48:52 +07:00
simon
8f72c5f42c implement cookie import, #build
Changed:
- fix arm64 ffmpeg issue
- added cookie import
- use cookie for all yt-dlp calls
- API: add cookie validation view
2022-04-30 19:34:10 +07:00
simon
14d85e83e6 move ffmpeg install to final container, #234 2022-04-30 19:33:11 +07:00
simon
cd139dfc1c API: add cookie validation view 2022-04-30 19:13:49 +07:00
simon
bc7d90f1f4 raise FileNotFoundError when trying to use unavailable cookie 2022-04-30 19:09:19 +07:00
simon
3a21690309 implement post_process_updated for settings, import cookie 2022-04-30 18:35:04 +07:00
simon
487c81d50b fix FileNotFoundError in cookie hide 2022-04-30 18:34:28 +07:00
simon
088e87bccf add cookiefile to PendingList and VideoDownloader 2022-04-30 17:27:57 +07:00
simon
6cc5fd8890 add cookiefile to yt_obs in YouTubeItem baseclass 2022-04-30 16:40:36 +07:00
simon
4e3d71e0e3 simplify, un-userspaced 2022-04-30 16:38:23 +07:00
simon
9b107b9940 implement cookie validator 2022-04-30 13:05:01 +07:00
simon
43eec5aac0 move CookieHandler to separate module 2022-04-30 12:05:41 +07:00
simon
430fdb6f1c add cookie import to frontend 2022-04-29 18:38:43 +07:00
simon
1fa26cdc44 added basic cookie import functionality 2022-04-23 21:59:59 +07:00
simon
fa9d6df406 api extension, #build
changed:
- add localhost to allowed hosts, flag to disable cors
- API: pagination
- API: sort and filter download list
- API: add task view
2022-04-23 20:53:51 +07:00
simon
eb7313fe6b API: add run task view 2022-04-23 20:50:38 +07:00
simon
71b3654942 add localhost:3000 to allowed 2022-04-23 20:17:31 +07:00
simon
3f99f7edff add localhost to allowed origin, optional cors disable 2022-04-23 20:16:28 +07:00
simon
b76f38e0bc API: fix downloads list sort 2022-04-21 05:45:55 +07:00
simon
1477370376 init data instead of class attribute 2022-04-20 22:51:30 +07:00
simon
04fc6ed26a API: add pagination 2022-04-20 22:43:07 +07:00
simon
7305216485 add link to ES documentation for disk usage 2022-04-19 08:07:47 +07:00
simon
3147df20da skip subtitle segments without duration, take 2 2022-04-18 11:52:13 +07:00
simon
d086f63861 API: sort and query filter download view, delete by filter 2022-04-17 20:10:49 +07:00
simon
40bb3e880e API: implement status update and delete of item in queue 2022-04-17 19:15:40 +07:00
simon
251e98cfa3 add channel-video playlist-video api, #build
Changed:
- Added /api/channel/channel-id/video/
- Added /api/playlist/playlist-id/video/
2022-04-17 10:08:45 +07:00
simon
eb6d6be3b9 handle 404 in channel-video and playlist-video api view 2022-04-17 10:08:24 +07:00
simon
b8ca324aaf add playlist-video and channel-video api views 2022-04-17 09:58:18 +07:00
simon
7d45d23767 process ta_download search results 2022-04-17 05:21:35 +07:00
simon
9224696e33 Merge branch 'api-fix' into testing 2022-04-17 05:03:05 +07:00
simon
50f4e0e5b7 skip base64 generator, hotfix 2022-04-16 13:08:32 +07:00
simon
fcdb31e848 bump release number 2022-04-16 09:30:40 +07:00
simon
70ac33928f update roadmap 2022-04-16 09:29:49 +07:00
simon
59f1c111aa extend timeout for sleep interval 2022-04-16 08:58:35 +07:00
Nathan DeTar
1cf82bdbbd Cleanup & Prevent repeat of skip message. (#227)
* Cleanup & Prevent repeat of skip message.

* skip sponsorblock segments if not indexed

* disable onVideoProgress sb check if not indexed

Co-authored-by: simon <simobilleter@gmail.com>
2022-04-16 08:53:15 +07:00
simon
ef803a157a standard api key processor for list and single views 2022-04-16 00:37:56 +07:00
simon
51ceffd58f update all links to tubearchivist organization 2022-04-15 22:35:13 +07:00
simon
f05c292170 add new home message 2022-04-15 21:35:44 +07:00
simon
b819c2f723 merge testing into master before repo migration 2022-04-15 21:16:28 +07:00
simon
fe610fdaca add log output for backup progress 2022-04-15 20:41:38 +07:00
simon
2fc4ed419e add note about tubearchivist-es to readme 2022-04-15 20:07:59 +07:00
simon
c84fbdfb9c fix _check_get_sb for per channel deactivate 2022-04-15 20:04:01 +07:00
simon
7231b5d245 implement unset of sb per channel overwrite 2022-04-15 20:03:15 +07:00
simon
3d16954c79 change to tubearchivist-es, add comments 2022-04-15 17:56:17 +07:00
simon
c5746089f5 auto dedect new es version 2022-04-15 17:46:16 +07:00
Ainsey11
dc29c6718c Fix: #210 - Download UI Box being removed when file is still processing (#219)
* Fix: https://github.com/bbilly1/tubearchivist/issues/210 - Download UI element is removed when the file has finished downloading, but is still being moved on disk to the storage destination. This means the user is presented with nothing in the UI yet the file is still being processed.

* FIX: Resolving linting issue

* FIX: Set moving message to never expire and then a second message after the move with a 4 second expiry timer.

Co-authored-by: Rob Ainsworth <roba@immjsystems.com>
2022-04-15 15:21:24 +07:00
simon
d4b1d97f5c add mappings for sponsorblock segments 2022-04-15 15:16:31 +07:00
simon
f71644c8bc guarantee stats in mapping for sort 2022-04-15 15:03:43 +07:00
simon
80c76c22ca add wiki documentation for sponsorblock integration 2022-04-14 16:49:27 +07:00
simon
4853a8b12b limit video_daily to below 10k in daily refresh 2022-04-14 16:18:08 +07:00
simon
fd00369859 fix last page pagination link building error, #221 2022-04-14 16:04:21 +07:00
simon
a8a7edb93e skip subtilte events without duration, #196 2022-04-14 15:53:10 +07:00
simon
5f90d21234 update roadmap, extend user created playlist functionality 2022-04-13 16:13:43 +07:00
simon
c59c1fc007 improved sponsorblock indexing, #build
Changed:
- improved more flexible sb timestamps indexing
- fixed per channel sb activate/deactivate
- improved Dockerfile with multi stage build
- added playlist API list view
2022-04-13 15:56:05 +07:00
simon
3007e02fe5 fix per channel sb integration 2022-04-13 15:53:00 +07:00
Nathan DeTar
39c4bd8883 Adjust to API changes for SB integration. (#222) 2022-04-13 10:30:32 +07:00
simon
cd3e9dd024 add playlist API list view 2022-04-13 09:51:15 +07:00
simon
8edde732b6 improved sponsorblock key with additional metadata 2022-04-12 17:47:13 +07:00
simon
3df1df8b5a set explicit mappings for sponsorblock key 2022-04-12 17:45:06 +07:00
simon
1c233dd4f4 move install requirements after ffmpeg curl for better cache use 2022-04-11 17:58:19 +07:00
simon
385f53372c bump django 2022-04-11 17:57:29 +07:00
simon
f79d30ab54 multi stage build Dockerfile for python wheel 2022-04-11 00:05:03 +07:00
Ainsey11
a8ee0da2b8 Addiing clarification on when the speed limit change takes effect (#218)
Relates to https://github.com/bbilly1/tubearchivist/issues/216 

Speed limits work as designed, however the wording in the tooltip is a little unclear to newer users about when the speed limit takes effect on their downloads
2022-04-10 22:33:00 +07:00
simon
0c84386209 add link to installation.md to home 2022-04-10 16:53:07 +07:00
simon
f52a00937f renamed to generic installation.md wiki page 2022-04-10 16:51:40 +07:00
simon
4b33559212 build failure workaround, upgrade pip and setuptools 2022-04-10 16:45:50 +07:00
simon
fd5de99674 implement sponsorblock skipping, #build
Changed:
- sponsorblock frontent implementation, #208
- per channel sponsorblock
- improved scheduler input validation
2022-04-10 16:26:47 +07:00
simon
582011d247 Merge branch 'master' into testing 2022-04-10 16:25:48 +07:00
Nathan DeTar
c316d05549 Added sponsorblock skipping (#208)
* Added sponsorblock skipping.

* Basic framework for sending SB timestamps

* Sponsorblock send timestamp UI improvements

* Added Sponsorblock Icons

* Minor UI tweaks

* Revert UI changes, implement in new UI

* Added notification when sponsor segment is skipped

* Add formatting for notifications & SB messages

* Added SB messages to JS player

* Added SB skip notifcation to videos page.

* Added SB messages to video page

* Change SB messages.

* Check channel_overwrites

* Check Per Channel Settings.

* Cleanup
2022-04-10 16:20:58 +07:00
crocs
77a19a62df added tumbnail reference in the wiki page (#213)
Co-authored-by: user.crocs <user.pairofcrocs@protonmail.com>
2022-04-10 16:19:39 +07:00
simon
31378ac756 better generic schedule validator for impossible input, #209 2022-04-10 15:58:11 +07:00
simon
10385b1414 bump bs4 2022-04-10 15:57:15 +07:00
simon
d8c7b3df0b better error message for wrong es version, #197 2022-04-09 14:12:03 +07:00
simon
99e0c1c90e bump yt-dlp 2022-04-08 23:15:58 +07:00
simon
236215cc4c add config var to video.html template 2022-04-08 22:10:16 +07:00
simon
552636d882 handle 404 in video item API view 2022-04-08 17:55:24 +07:00
simon
9d73dbc45a implement video overwrites index for sponsorblock 2022-04-08 17:19:25 +07:00
simon
859bf2a28d add target _blank to external links 2022-04-08 17:18:33 +07:00
simon
365ebf53a5 implement per channel sponsorblock 2022-04-08 17:17:39 +07:00
simon
fe3e3cfacc fix filenotfounderror for missing subtitles when deleting video 2022-04-08 15:56:34 +07:00
simon
efa240440b fix userspace for vide base config 2022-04-08 15:15:59 +07:00
simon
e2f4dd124a bump beautifulsoup4 2022-04-08 15:15:37 +07:00
simon
90337745bf Merge branch 'master' into testing 2022-04-08 14:17:10 +07:00
crocs
f885965c2b Merge pull request #207 from pairofcrocs/patch-7
Create AlternativeInstallation.md
2022-04-07 15:46:58 -05:00
simon
a07d789e66 add base64 blur video thumb 2022-04-08 00:29:09 +07:00
simon
6a1cb15114 validate hours to not be greater than 23, #209 2022-04-07 23:02:07 +07:00
simon
b1a7a6a148 use cleaned_data for config form parser 2022-04-07 22:30:20 +07:00
simon
39a6fbea74 add links to roadmap issues 2022-04-05 22:56:47 +07:00
simon
573c1514bf simulate sponsorblock voting api endpoints, #build
Changed:
- added simulated post sponsorblock api endpoints
- updated api documentation
- updated roadmap
2022-04-05 22:34:51 +07:00
simon
348a114981 update Roadmap 2022-04-05 22:32:55 +07:00
simon
a84e657577 add documentation for sponsorblock api endpoints 2022-04-05 22:27:55 +07:00
simon
03dd25cff4 implement vote on sponsorblock segments api 2022-04-05 22:25:40 +07:00
simon
286b1cf9b6 simulate sponsorblock post request 2022-04-05 21:51:45 +07:00
simon
bace7d41af add random string generator helper function 2022-04-05 21:51:10 +07:00
simon
738b083a7f create dedicated SponsorBlock class to handle integration 2022-04-05 21:50:40 +07:00
simon
1ac92254ad add global vars for version and url 2022-04-05 18:43:15 +07:00
simon
e404bac7c3 bump celery 2022-04-05 18:35:06 +07:00
simon
0a839ff960 Merge branch 'testing' 2022-04-05 00:05:47 +07:00
simon
a8b62e5cdc better error message on sponsorblock fail 2022-04-04 23:28:32 +07:00
simon
730d12212f implement basic sponsblock indexing, #119 2022-04-04 23:28:32 +07:00
simon
29f8e148da bump libraries 2022-04-04 23:28:32 +07:00
simon
71c441d871 better error message on sponsorblock fail 2022-04-04 23:24:51 +07:00
crocs
00750500a1 Add files via upload 2022-04-04 11:16:24 -05:00
simon
77228ccb65 implement basic sponsblock indexing, #119 2022-04-04 23:08:44 +07:00
crocs
f0fba0644d Create AlternativeInstallation.md 2022-04-04 11:08:36 -05:00
simon
2292e6506d bump libraries 2022-04-04 22:55:43 +07:00
simon
4775c9d830 connection test API endpoing, #build
Changed:
- added /api/ping/ to test your connection
- moved browserextension to dedicated repo
- merges nginx ipv6 fix, #203
2022-04-03 08:24:38 +07:00
simon
cae00b032b return better data for ping api, add to readme 2022-04-01 16:24:40 +07:00
Lickitysplitted
3ae9fe5405 Modify Dockerfile and the nginx.conf file to avoid Nginx default conf… (#203)
* Modify Dockerfile and the nginx.conf file to avoid Nginx default config IPv6 conflict.

* Adjust nginx.conf name and run.sh nginx modification.

* but nginx.conf file back

Co-authored-by: simon <simobilleter@gmail.com>
2022-04-01 11:43:24 +07:00
simon
12fa5748d4 move extension to tubearchivist/browser-extension 2022-04-01 08:48:42 +07:00
simon
ab9c02dfad redefine scope of current state 2022-03-31 23:39:46 +07:00
simon
4de5b2d091 Merge branch 'browserextension' into testing 2022-03-31 23:29:11 +07:00
simon
ed6521acad reimagine browser extension, simple download button 2022-03-31 23:28:53 +07:00
simon
9ab6955125 add connection test api view 2022-03-31 21:13:21 +07:00
simon
46622d9ab2 bump redis lib 2022-03-31 21:13:04 +07:00
simon
607d9b3030 add sync_latest_es function to update tubearchivist-es:latest container 2022-03-31 14:47:19 +07:00
simon
4eb2fb01d9 a note about SSO, LDAP, 2FA in the FAQ 2022-03-30 17:49:03 +07:00
simon
3caafc6e21 update roadmap, rewording 2022-03-30 17:44:54 +07:00
simon
8a3349c6a5 a note about transcoding quality, #138 2022-03-30 17:27:53 +07:00
simon
b46910ec90 update roadmap, add issue links 2022-03-30 17:21:25 +07:00
simon
1cd285aec4 parse api views, #build
Changed:
- return frontend relevant paths for api views
- add video api list view
2022-03-29 18:36:54 +07:00
simon
e51193f988 update roadmap 2022-03-29 18:35:01 +07:00
simon
0783d47ffe bump python version 2022-03-29 18:34:31 +07:00
simon
6f5969f520 add video list api view 2022-03-29 17:05:22 +07:00
simon
0c8fa8f49e clean up prints 2022-03-29 17:03:34 +07:00
simon
80af255f25 reinvent SearchProcess class for API endpoints 2022-03-29 16:48:04 +07:00
simon
cdc617c382 add datestr generator 2022-03-29 16:47:21 +07:00
simon
f1fb504874 fix spacing 2022-03-29 10:33:43 +07:00
simon
e04690d96d add API login view, #build
Changed:
- get token and userid from api endpoint
2022-03-29 10:18:56 +07:00
simon
5dce2441fa add API login view 2022-03-29 10:17:47 +07:00
simon
5d8e314983 be more explicit what you are searching now 2022-03-28 12:43:57 +07:00
simon
9e37c0fab1 add ibracorp review video 2022-03-27 19:43:40 +07:00
simon
cc49c256c1 note about min ES version 7.17 2022-03-27 18:28:17 +07:00
simon
dc8e330b22 tail git tag output, too many versions now 2022-03-27 18:27:36 +07:00
simon
358194468e increase version to v0.1.3 2022-03-26 19:47:50 +07:00
simon
1d3d3b13ed reduce max-complexity to 10 2022-03-26 18:36:10 +07:00
simon
f15bb50cfe fix playlist reindex after PendingList refactor 2022-03-26 18:31:00 +07:00
simon
fcadb5ead8 add random headers for requests outside of yt-dlp 2022-03-26 11:49:53 +07:00
simon
6d874f4b7a add section for per channel customization 2022-03-26 11:13:48 +07:00
simon
a0ed88580f fix index validate_mappings for new nested object 2022-03-26 10:52:57 +07:00
simon
9537c388ab Merge branch 'master' into testing 2022-03-25 17:31:59 +07:00
simon
40d7e6da37 add secondary sort for channel and title, #193 2022-03-25 17:28:17 +07:00
simon
fa60f2e5f8 Style channel configure form, #build
Changed:
- layout of the channel configure form
- hide if no overwrites set
- validate min es version 7.17.x
- improved build to incl debug tools
2022-03-25 15:44:01 +07:00
simon
e4b4430e69 bump python version 2022-03-25 15:33:54 +07:00
simon
bcf9185bd3 implement es version check at startup 2022-03-25 15:33:09 +07:00
simon
337b373628 improve build to install debug tools in testing 2022-03-24 19:54:47 +07:00
simon
59d58f8866 bump redis version 2022-03-24 19:51:45 +07:00
simon
d0f826485f move and rename configure button 2022-03-24 16:10:40 +07:00
simon
d5cd90eb34 style and toggle hide overwrite form 2022-03-24 15:43:15 +07:00
simon
8d1d09e698 extend IndexPaginate to optionally return _source key 2022-03-23 17:17:42 +07:00
simon
fb4c11bd38 refactor ElasticIndex and ElasticBackup to use ElasticWrap and IndexPaginate 2022-03-23 17:17:17 +07:00
simon
bfcc538ed1 fix concurrency issue with subtitle indexing 2022-03-23 16:20:33 +07:00
simon
912c19f6cf use ElasticWrap in FilesystemScanner 2022-03-23 15:56:53 +07:00
simon
fda520ad44 refactor and consolidate Reindex class 2022-03-23 15:48:38 +07:00
simon
1f7d6871cf use ElasticWrap to delete item from pending 2022-03-23 11:09:21 +07:00
simon
5b1c9c64de cleanup, use ElasticWrap 2022-03-23 09:44:31 +07:00
simon
f802c4d596 remove logging 2022-03-22 18:43:29 +07:00
simon
192d379a3e refacter api views to use ElasticWrap 2022-03-22 18:43:16 +07:00
simon
9f5c9b17a5 refactor redis connection, fix sync_redis_state task setup issue 2022-03-22 17:50:54 +07:00
simon
fa25a56126 fail open for in continue watching videos building 2022-03-22 13:07:22 +07:00
simon
75cd9d382d fix duration builder on reindex by ignoring vtt files 2022-03-22 12:59:39 +07:00
simon
22a3d3f6cd implement per channel overwrites, #build
Changes:
- merges per channel overwrite for:
  - download format
  - delete after x days
  - playlist index
- merges major refactor of download queue handeling classes
- merges improved startup checks
2022-03-22 11:49:30 +07:00
simon
fd4f15ab66 refactor StartupCheck into class, lock if already run for multithreading 2022-03-22 10:35:36 +07:00
simon
1f1dfcb54f better playlist scan message, private methods in YoutubeChannel class 2022-03-22 10:07:33 +07:00
simon
3d451e47e5 full playlist refresh and index for channel overwrites 2022-03-22 09:42:41 +07:00
simon
fc10fa62e8 remove now redundant find playlist botton 2022-03-21 19:07:58 +07:00
simon
875c601f4e implement index channel playlist from form 2022-03-21 18:27:00 +07:00
simon
91a9477bba refactor index_channel_playlists task, move to YoutubeChannel class 2022-03-21 17:59:21 +07:00
simon
c9399f61d0 bump es version 2022-03-21 17:04:38 +07:00
simon
fc311a7b7e refactor validate_playlists and move into DownloadPostProcess class 2022-03-21 11:14:44 +07:00
simon
f803c5298b fix missing update_status method call in ignore video 2022-03-21 11:09:16 +07:00
simon
e67d576d79 fix missing video list in index_channel_playlists 2022-03-21 10:47:15 +07:00
simon
7c6041d686 refactor VideoDownloader and implement per channel delete older than 2022-03-19 17:37:28 +07:00
simon
1411262793 remember channel overwrites on rescan, remove unused rescape_all_channels method 2022-03-19 15:12:29 +07:00
simon
9fd7e2b11a rename channel_overwrites attribute 2022-03-19 15:03:38 +07:00
simon
3ba6e0478d fix filesysem rescan function 2022-03-19 14:36:46 +07:00
simon
b68a00073f fix thumbnail validation with new PendingList class 2022-03-19 14:27:37 +07:00
simon
89428297c9 add download format channel overwrite per video 2022-03-18 22:22:41 +07:00
simon
a7945e30e3 fix building missing vid ids list 2022-03-18 22:11:41 +07:00
simon
ac9df4e082 map channel overwrite to video id for later efficient mapping 2022-03-18 21:39:33 +07:00
simon
a6937db5fd use the refactored PendingList class 2022-03-18 18:27:25 +07:00
simon
05b8dbc02f refacor PendingList class into subclasses 2022-03-18 17:19:21 +07:00
simon
1498fadf27 implement channel playlist index overwrite 2022-03-16 12:32:42 +07:00
simon
f0e82caebb add bool true to channel overwrite form parser 2022-03-16 12:32:02 +07:00
simon
2ef8823c2d better channel overwrite update form 2022-03-16 12:09:50 +07:00
simon
dc67293052 add fallback channel overwrite fallback to frontend 2022-03-15 17:45:43 +07:00
simon
544d842a4a rename channel format overwrite field 2022-03-15 17:45:15 +07:00
simon
774220ab1f add channel overwrite form to frontend, store in es 2022-03-15 12:00:48 +07:00
simon
105d5bf3f7 add auto label to subtitle track 2022-03-13 22:56:20 +07:00
simon
026cc378fe add get_overwrites and set_overwrites methods for channel 2022-03-12 22:19:42 +07:00
simon
8170d4913b add fallback total video length if ffmpeg extraction failes 2022-03-12 21:53:03 +07:00
simon
34708dd59f Subtitle parser rewrite, #build
Changes:
- merges fix for progress bar issue on player close
- rewrite subtitle parser to use json3
- combining 5 cues into single es document for more efficient indexing
2022-03-12 20:37:54 +07:00
simon
7595e7501f sort continue watching videos 2022-03-12 20:29:26 +07:00
simon
f6950a2ca5 list all in progress videos 2022-03-12 17:29:34 +07:00
simon
aff0cfb794 fix retiming issue for auto subtitles 2022-03-11 17:47:04 +07:00
simon
d3e9646fb6 private methods for YoutubeSubtitle and SubtitleParser 2022-03-10 20:45:13 +07:00
Nathan DeTar
6e3df21f8c Continue Watching Section (#188)
* Replaced isWatched() function.

* Switched to `updateVideoWatchStatus()` function

* Updated Onclick to `updateVideoWatchStatus(this)`

* Handle `this` input in `updateVideoWatchStatus()`
2022-03-10 20:20:23 +07:00
simon
1ce832b846 fix autocaption extraction, flatten words, #180 2022-03-10 19:58:13 +07:00
simon
6a6c8fa5d8 bump yt-dlp version 2022-03-10 17:39:35 +07:00
simon
c186798e78 rewrite SubtitleParser, #180 2022-03-09 00:25:44 +07:00
simon
8c4607fee9 Continue Watching, #build
Changed:
- merges progress bar fix on 100%
- merges new content area on home for continue watching
2022-03-07 20:58:46 +07:00
simon
40c8e6d146 standardize template jinja indentation 2022-03-07 20:58:17 +07:00
simon
fb12a32d4f use generic data-id and data-status attributes for watched checkbox 2022-03-07 20:48:45 +07:00
simon
437e83b2ae add continue watching section to home page 2022-03-07 20:26:37 +07:00
simon
25bc66ae80 bump django version 2022-03-07 20:23:23 +07:00
Nathan DeTar
ad5e74cb27 Prevent setting progress bar on player close if video is watched. (#182)
* Prevent setting progress bar on close if watched.
2022-02-26 18:11:09 +07:00
simon
6822ed380d Merge video progress bar to master 2022-02-26 09:26:53 +07:00
simon
b28905bbae bump footer version v0.1.2 2022-02-25 12:27:16 +07:00
simon
36a2996cda clean exit of progress builder when no videos in channel 2022-02-25 12:20:22 +07:00
simon
b98ccd9dab handle error if subscribed channel not available anymore, #175 2022-02-25 11:21:29 +07:00
simon
060f0d575e change color of progress bar to dark 2022-02-25 10:51:37 +07:00
Nathan DeTar
99c97a703f Reduce API Calls (#181)
* Reduce API calls

* Fix video id

* Updated `createVideoTag()` description.

* Fixed URL used for cast integration

* Check video duration

* Updates progress bar on watched and close.

* Set progress bar width to 0% by default

* Cleanup, function descriptions

* Cleanup console logging

* Update progress bar on cast progress every 10s

* Catch short <30s videos and mark as watched
2022-02-25 10:39:33 +07:00
simon
806448624d return if no in progress videos 2022-02-24 21:25:12 +07:00
simon
df777104af add video progress bar to channel_id and playlist_id views 2022-02-24 19:30:12 +07:00
simon
d88d6d6a61 add video progress bar 2022-02-24 18:58:26 +07:00
simon
6078d8d276 add youtube_id to progress api key 2022-02-24 18:55:52 +07:00
simon
c4d6bb35a3 add list_items for wildcard matching 2022-02-24 18:55:18 +07:00
simon
a25b101c3a remember video progress, #build
Changes:
- merges video progress API endpoint
- merges fix for auto subscribe reset of playlists
- merges fix for ffmpeg install link builder
2022-02-24 09:43:04 +07:00
simon
e0db73543e Merge branch 'master' into testing 2022-02-24 09:39:56 +07:00
Nathan DeTar
4812b8da55 Save Video Progress (#179)
* Added cast integration docs.

* Changed header sizes.

* Added more space above Requirements

* Added cast integration docs.

* Removed separate cast integration docs

* Further indented quote from Google

* Switch to HTML based video position.

* Ground work for API changes and video progress

* Added onpause attribute to video.

* Added save video progress feature.

* Added API check for subtitle status.

* Switch method to DELETE if position is 0

* Added `createVideoTag()` function

* Added `InsertVideoTag()` function

* Switch to JS generated video tag, add on page load

* Removed extra data from DELETE request

* Removed unused code

* Reduced duplicate code

* Cleanup & groundwork cast pull metadata from API

* Minor bug fix

* Fix saving video progress on player close.

* Only send video progress when unwatched

* Cleanup

* Added `getURL()` function

* Cast use API & save progress/mark as watched

* Added cast progress checks

* Changed thresholds for marking videos as watched

* Added `watchedThreshold()` function
2022-02-24 09:36:31 +07:00
simon
8579fb4cc1 fix ffmpeg link grep by using master 2022-02-21 21:37:49 +07:00
simon
b8b95f9d79 inheirt VideoProgressView from base class to get auth 2022-02-21 20:59:04 +07:00
schizovivek
70506ad8f6 add comment for redis rejson to use arm64 fork created by bbilly1 (#178)
* updated redis rejson to use arm64 fork created by bbilly1

* added arm build as a comment instead of main option for rejson
2022-02-21 20:14:08 +07:00
simon
ec00568008 fix auto reset of playlist_subscribed 2022-02-20 19:22:11 +07:00
simon
2044dba700 API endpoints improvement, #build
- merges adding config key to api return values
- add video progress api endpoint
2022-02-17 18:21:56 +07:00
simon
241d8326f7 add video progress API endpoints 2022-02-17 18:20:30 +07:00
simon
4d83af7c14 bump redis client 2022-02-17 18:19:52 +07:00
simon
265795f4a9 add config key to api response 2022-02-17 16:59:59 +07:00
Nathan DeTar
5c3f0d1e5f Docs for the cast integration (#172)
* Added cast integration docs.

* Changed header sizes.

* Added more space above Requirements

* Added cast integration docs.

* Removed separate cast integration docs

* Further indented quote from Google
2022-02-14 17:10:25 +07:00
simon
047fc1f3f1 bump release version 2022-02-13 10:58:58 +07:00
simon
8fce31b983 fix reindex and deactivate error 2022-02-13 10:05:08 +07:00
simon
8f87c4774a restructure and update roadmap 2022-02-12 21:59:51 +07:00
simon
fef866bf0b indicate max page size on downloads page, add note to wiki, #156 2022-02-12 19:42:07 +07:00
simon
428cc315e4 Subtitle improvements, #build
Changes:
- merges subtitle suport for JS video player
- merges hint what to do when no videos found
- merges better indexing and error handeling of subtitles
2022-02-12 19:11:47 +07:00
Nathan DeTar
385d6bace8 Fix cast support to handle new video tag format. (#169)
* Added subtitle support to JS player.

* Move `video-item` id to source tag.

* Move `video-item` id to source tag.

* Fix cast support to handle new video tag format

* Add subtitle support to cast integration, WIP

* Replace `&amp` with `&` in video titles.

* Check if the video is already marked as watched

* Switch to HTML watched check.
2022-02-12 19:08:19 +07:00
simon
78720b33b7 clean exit when auto subtitle extract fails 2022-02-12 18:52:03 +07:00
simon
ce4fa8ee61 delete existing before of reindexing subtitles 2022-02-12 18:22:01 +07:00
simon
13cbada539 add section about subtitles and extend integrations 2022-02-12 17:09:25 +07:00
simon
a5f6be16e1 switch subtitle source order 2022-02-12 17:09:05 +07:00
simon
5abf988810 add FAQ wiki page 2022-02-12 16:27:31 +07:00
simon
b494fc10af cover edge cases where end timestamp is after start timestamp of new cue 2022-02-12 16:03:04 +07:00
simon
d99ce0d98e add atomicparsley to container for better thumb embed, #155 2022-02-12 09:58:14 +07:00
simon
09500557c8 open fail on subtitle line parse error 2022-02-11 18:47:29 +07:00
simon
7524691b79 implement API token revoke 2022-02-11 18:19:10 +07:00
simon
f1de8db4f3 hide API token by default 2022-02-11 17:18:37 +07:00
simon
08f5248e7a make subtitle index optional 2022-02-11 13:36:36 +07:00
simon
1abe4fb4d6 add text/vtt content-type header to subtitle files 2022-02-11 13:06:13 +07:00
Stephen Herbein
b1435434e6 docs: add a note that importing requires there be no subdirectories (#170)
Also include some bash snippets to help users with existing video
libraries containing subdirectories get their files all in one directory
quickly.

Co-authored-by: Stephen Herbein <sherbein@DESKTOP-EAJL82P.localdomain>
2022-02-11 12:55:14 +07:00
Stephen Herbein
4467e97f3c When no videos are found, add a hint to start a search/download (#167) 2022-02-11 12:52:26 +07:00
simon
af8e01cd8c fix clean exit if no subtitles selected 2022-02-11 09:03:56 +07:00
Nathan DeTar
656a0c7327 Added subtitle support to JS player. (#168) 2022-02-11 08:53:09 +07:00
simon
3efa388b5a add subtitle functionality, #build
Changes:
- merges new subtitle download and index functionality
- merges player improvements and api integrations from @n8detar
- merges fix for non ascii channel names
- merges fix for pagination error with 10k+ videos
2022-02-10 19:48:39 +07:00
simon
16f33feda0 process subtitle media url paths 2022-02-10 19:45:22 +07:00
simon
3ea5e9c537 bump dependencies 2022-02-10 19:27:05 +07:00
simon
4d30bed3cc extend delete video to also delete subtitles 2022-02-10 19:09:07 +07:00
simon
0e56efc428 limit filesystem scan to mp4 files only 2022-02-10 18:48:35 +07:00
simon
077692987b fix multi language subtitle extractor, and better regex for timestamp matching 2022-02-10 18:32:23 +07:00
simon
b071612038 better error raising for add player info 2022-02-10 17:34:21 +07:00
simon
0414df0de0 fix key error for subtitle source 2022-02-10 17:10:30 +07:00
simon
a2cae51f48 bulk import subtitle lines into es 2022-02-10 17:02:19 +07:00
simon
9f652802ae add new mapping for subtitle index 2022-02-10 11:47:14 +07:00
simon
4e4cfe3334 pass whole video object into YoutubeSubtitle class 2022-02-09 23:40:15 +07:00
simon
4e2d0fa464 bump es version 2022-02-09 23:38:18 +07:00
simon
6cb892a811 integrate auto generated subtitle cleaner 2022-02-09 21:33:41 +07:00
simon
5f6158243e auto generated subtitle parser and cleaner 2022-02-07 21:18:52 +07:00
simon
1664b0d4fc restructure video tag to add subtitle tracks 2022-02-06 00:08:24 +07:00
simon
e98ffc0050 add subtitles mapping to video index 2022-02-05 23:50:47 +07:00
simon
52013aff3f fix subtitle download of first video of channel without folder 2022-02-05 23:42:42 +07:00
simon
44af78b7e3 handle NA in ffprobe duration extractor 2022-02-05 23:09:05 +07:00
simon
2bf9e9683b error handeling in _normalize_lang to skip livechat and ignore missing 2022-02-05 22:51:38 +07:00
simon
f5f46349b2 handle rescan name change 2022-02-05 22:38:59 +07:00
simon
851fbae900 fix video template dislike icon and add watched icon 2022-02-05 18:42:09 +07:00
simon
91452b5114 remove redundant video player api endpoint 2022-02-05 18:35:02 +07:00
simon
5b37bd059c Merge branch 'testing'
combine new api endpoints from #151
2022-02-05 18:27:24 +07:00
Nathan DeTar
9079a2a78b Get Video Player Data Using New API (#151)
* Get  video player data using new API

* Spelling

* Removed extra data from play button

* Reworked createPlayer, switched functions to API

* Add theme to scrollbar

* Removed extra metadata from playlist page

* Removed extra metadat from channel page

* Reworked createPlayer, switched functions to API

* Update style.css

* Changed watched indicator to match createVideo()

* Fixed createPlayer() watched button

* Fix watched indicator duplication

* Minor clean up

* Removed player-wrapper background

* Added video/channel info to generated player

* Removed description due to textReveal() conflict

* Mark video as played at 90% playback

* Groundwork for saving video playback

* Add half and empty stars to getStarRating()

* Check videoProgress input.

* Added last refresh and date published

* Switched date in create functions to API

* Fomatted dates to match the old format

* Remove console log from formatDates()

* Cleaned up error on video player close

* Added check for ryd dislikes/rating

* Refined ryd check

* Simplified player

* Added player stats css formatting

* Formatting for playlist name/link

* Add playlist title/link to player

* Commented out no longer used code

* Fix missing end `"` on video-player class

* Additional playlist error checking

* Change setting video progress to html method

* center thumbs icon, add eye icon for watched

* add playerStats builder example, change some spacing

* Removed `-` before playlist, reordered cast button

* Minor cleanup of unused code.

* Corrected POST data formating

* consolidate video api calls into one

* remove redundant api calls for search result population

* do some jshint

* shorten unit and add K to formatNumbers

Co-authored-by: simon <simobilleter@gmail.com>
2022-02-05 18:26:31 +07:00
simon
8fe00e2152 process api return values for frontend use 2022-02-05 17:46:14 +07:00
simon
ac531affb5 standardize country specific subtitle language codes 2022-02-05 16:07:11 +07:00
simon
3ef35a9d53 raise FileNotFoundError to catch for reindex 2022-02-05 09:30:34 +07:00
simon
7aaf140ccb index subtitle url to video 2022-02-04 17:14:00 +07:00
simon
ad2647c4ba upgrade libs 2022-02-04 17:13:30 +07:00
simon
a82e78f8bf index selected subtitles if available 2022-01-30 23:57:58 +07:00
simon
365a2bf59f add frontend subtitle dl settings 2022-01-30 20:10:29 +07:00
simon
e5e83287ab fix channel delete for channel_id fallback folders 2022-01-30 08:33:10 +07:00
simon
8e860d4f01 fix last page error for more than 10k results, #156 2022-01-27 23:39:07 +07:00
simon
0749202d5d add fallback for none ascii channel names, #127 #146 2022-01-27 22:50:02 +07:00
simon
160676acc1 add mapping for channel_tvart_url 2022-01-27 21:56:48 +07:00
simon
8591c44ef2 major refactor, #build
Changes:
- merges new restructured and split up modules
- merges refactor channel, video, playlist index classes
- merges code clean up and readability improvements
2022-01-27 16:08:16 +07:00
simon
2b2ff814e3 red hover logout button 2022-01-27 16:04:04 +07:00
simon
87b72a571d simplify reading json files 2022-01-27 15:32:58 +07:00
simon
00e9e4bc53 bump python version 2022-01-27 15:08:26 +07:00
simon
b93a6f689b remove previous bug workaround to set django debug env, #159 2022-01-27 14:49:22 +07:00
simon
b2f69d1433 untrack vscode folder 2022-01-26 20:21:08 +07:00
simon
2eea07c85e organize docker conf files 2022-01-26 20:05:52 +07:00
simon
b6f9fb58ad refactor VideoDownloader class, better obs builder 2022-01-26 19:37:36 +07:00
simon
2fc0cbacf5 update doc strings to represent new module structure 2022-01-23 19:32:08 +07:00
simon
0fc0cc8e87 major refactor, split up modules 2022-01-22 22:13:37 +07:00
simon
f3efca7464 bump yt_dlp version 2022-01-22 17:57:36 +07:00
simon
c57d6c73cc remove bandit, too many false positive 2022-01-22 17:52:13 +07:00
simon
b64a2f8195 squash index.py refactor commits 2022-01-22 17:48:54 +07:00
simon
2f4d4e715b refactor index class in to base and video classes 2022-01-19 13:04:15 +07:00
simon
44733adf4d Merge branch 'master' into testing 2022-01-18 17:28:50 +07:00
Nathan DeTar
26bc2d2af4 Add theme to scrollbar. (#153)
* Add theme to scrollbar.

* add firefox scroll bar color

Co-authored-by: simon <simobilleter@gmail.com>
2022-01-18 17:27:49 +07:00
simon
68f19b1719 make SearchHandler use new ElasticWrap class 2022-01-18 13:37:22 +07:00
simon
3eb0353fa9 new base class to make all es calls from 2022-01-18 13:36:41 +07:00
simon
af2783c18a remove unneeded auth 2022-01-18 13:35:48 +07:00
simon
8b15ea5dc8 bump redis version 2022-01-18 11:27:24 +07:00
simon
3d26f320bf new logout icon 2022-01-18 11:26:26 +07:00
simon
7028621bc5 merge new api view, #build
Changes:
- merges new cast improvements
- merges toggle invert
- merges new video player api endpoint
- merges basic browser extension example
2022-01-15 14:19:11 +07:00
Laurant Marijnissen
385771cba0 invert the hide button to more clearly show what it is currently set as. (#150) 2022-01-15 14:18:23 +07:00
simon
79b0a989be new api view to return video player 2022-01-15 14:14:18 +07:00
simon
cccb8e570a Merge cast changes from 'master' into testing 2022-01-15 13:35:16 +07:00
Nathan DeTar
b7922d171d Improved Cast Support (#143)
* Added cast integration requirements.

HTTPS and a supported browser are required for this integration.

* Improved cast handeling.

* Theme cast button.

* Improved cast support.

* Added proper cast button.

* Moved cast button location

* Moved button location

* Reorder cast button

* Fix typo

* Revert cast setting description.

* Match master branch

* Added comments

* Added id `cast-script` to cast-videos.js

* Reworked event listener

* Add cast  button to home player

* Check if active media on cast, pause browser video

* Commented out console logging

* Uncommented cast failed console log

* Cast video at current playback position

* use theme vars for cast color buttons

* add cast variable to base ArchivistViewConfig class

Co-authored-by: simon <simobilleter@gmail.com>
2022-01-15 13:33:16 +07:00
simon
e1cdd79b45 added browser extension proof of concept 2022-01-15 13:03:44 +07:00
simon
fec6237907 add chrome-extension to CORS_ALLOWED_ORIGIN 2022-01-15 12:27:36 +07:00
simon
3df8f65db6 implement cors for browser extension 2022-01-14 14:46:00 +07:00
simon
c703105c5e fix ryd 404 error for reindex and deactivate 2022-01-11 18:54:40 +07:00
simon
51f63f7b71 Merge api app from branch 'testing' 2022-01-11 18:43:03 +07:00
Nathan DeTar
45518dc3d2 Basic Google Cast Support (#140)
* Added Start and Stop cast buttons.

* Modified from the internet.

* Code to initialize casting

* Added `video-item` id to video

* Renamed function to make more sense

* Renamed cast functions to make more sense

* Renamed cast functions to make more sense

* Changed console logging message.

* Make cast buttons appear if enabled in settings.

* Make cast scripts only load if enabled in settings

* add cast configuration form, #140

* fix spelling

Co-authored-by: simon <simobilleter@gmail.com>
2022-01-11 18:31:22 +07:00
simon
c4a547f407 cleanup and typos 2022-01-11 17:05:04 +07:00
simon
b28773b3a0 add exmples, auth docs, and list endpoints 2022-01-11 16:53:58 +07:00
simon
50006e423c implement channel list and subscribe api 2022-01-11 16:53:02 +07:00
simon
8d5b4ac242 implement api return status code 2022-01-11 16:16:28 +07:00
simon
52a54fbe31 add to queue api endpoint 2022-01-11 15:58:50 +07:00
simon
382e89abb7 implement api token auth 2022-01-11 14:15:36 +07:00
simon
917e73ec4d new django api app, implementing basic get views 2022-01-10 22:51:52 +07:00
Nathan DeTar
edc9f3fe15 Added a button to download a video to the local device. (#137)
* Add video download button.

* Swapped <a> and <button> tags to fix theme issue.

* Download button renamed to `Download File`
2022-01-10 09:00:29 +07:00
Sean
8410b9310f feat(docker): uses named volumes in docker-compose (#133)
Closes #132
2022-01-10 07:54:23 +07:00
Crocs
fb2a7d59d4 Update README.md (#136)
misspelled :)
2022-01-10 06:25:26 +07:00
simon
ee2025664f update roadmap 2022-01-09 08:44:16 +07:00
simon
b8c2c67707 fix unstable build arch issue for docker_publish 2022-01-09 08:42:59 +07:00
Sean
28923ea9c7 docs: add helm chart alternative install method (#130) 2022-01-09 08:33:33 +07:00
simon
21a76595ec fix new screenshot links in readme 2022-01-09 00:02:02 +07:00
simon
e8a533bbbe add social links and update to v0.1.0 2022-01-08 23:58:41 +07:00
simon
9ad8177b7a retry failed commit 2022-01-08 23:57:51 +07:00
simon
709c7b88f5 wiki refresh for new search functionality 2022-01-08 21:21:49 +07:00
simon
8ba9ced833 move ryd to avoid celery missing redis var 2022-01-08 21:10:38 +07:00
simon
f62e75f343 add social links 2022-01-08 20:14:02 +07:00
simon
1a95797997 improve scheduler validation with regex, #126 2022-01-08 20:04:51 +07:00
simon
c660edd997 bump library versions 2022-01-07 18:35:59 +07:00
simon
9498cf94f3 fix playlist view_origin 2022-01-07 18:30:40 +07:00
simon
099f9dfe00 make search results follow default view styles 2022-01-07 18:29:25 +07:00
simon
1cbe0bbfc0 delete playlists of channel when deleting channel, #118 2022-01-05 15:39:41 +07:00
simon
49cd272557 size limit for ryd missing rating query 2022-01-05 15:10:33 +07:00
simon
1c27934d77 remove redundant lock 2022-01-05 14:51:02 +07:00
simon
c85be45846 extend ryd to refresh when missing average_rating 2022-01-05 14:43:24 +07:00
simon
7a1f77e548 bump library versions 2022-01-05 14:42:28 +07:00
simon
3999d679e3 fix linting 2022-01-05 14:09:35 +07:00
simon
dc2fb65312 add optin returnyoutubedislike.com integration 2022-01-05 14:06:07 +07:00
simon
6aef3dfb96 remove leftovers 2022-01-05 11:10:11 +07:00
simon
49eb565e2a fix sort_by selected attribute and animation toggle 2022-01-02 16:25:05 +07:00
simon
2ce90077b6 fix notification for nownload now button 2022-01-02 15:47:32 +07:00
simon
fd334bcafe fix legacy import alias for yt-dlp 2022-01-02 15:38:45 +07:00
simon
6d16d75ef9 bump es version 2022-01-02 12:24:25 +07:00
simon
d5b3b90202 fix missing auth of search, reduce nginx alias to relevant paths only 2022-01-01 22:17:44 +07:00
simon
37140551d2 improved build cache add nginx after pip 2022-01-01 22:16:34 +07:00
simon
08f74caef0 simplify the sort_order and sort_by and standardize player location in page 2022-01-01 18:13:12 +07:00
simon
f7fce001c4 simplify the subscribe buttons 2022-01-01 17:25:57 +07:00
simon
4915aa0c11 remove now redundant search forms 2021-12-31 13:42:07 +07:00
simon
5616d3ee0d add search page link to top move about to footer 2021-12-31 12:41:54 +07:00
simon
e823d6e1b3 add multi search js functions to dynamically populate results 2021-12-30 22:13:47 +07:00
simon
e8eb7077ed add initial search endpoint and improve results$ 2021-12-30 20:42:42 +07:00
simon
5fb15b79a4 extend SearchForm class with multisearch method for frontend 2021-12-30 15:58:15 +07:00
simon
2a774c3d3e add english analyzer for main text fields for serching 2021-12-30 15:54:20 +07:00
simon
b6424e0e56 update requirements libs 2021-12-30 15:53:38 +07:00
simon
386220ee94 consolidate request attributes into parent class 2021-12-27 17:23:39 +07:00
simon
4c46f0bdbd refactor playlist and playlist-id views inheritance, #116 2021-12-27 16:58:08 +07:00
simon
30cf54c49c refactor channel and channel-id views inheritance, #116 2021-12-27 15:23:52 +07:00
simon
fb512b26ea further consolidate by adding query results in parent class 2021-12-27 12:01:49 +07:00
simon
2ae25540d1 refacter DownloadView to use new inheritance structure, #116 2021-12-27 10:58:49 +07:00
simon
6229d92333 add dedicated default_view for playlists 2021-12-26 22:51:37 +07:00
simon
6e348892f2 refactor home view by inherinting from dedicated view classes, #115 #116 2021-12-26 22:49:00 +07:00
simon
665d0a1b24 bump versions 2021-12-26 18:22:04 +07:00
simon
42f4f4cb6f improved sort order mobile layout on home 2021-12-18 18:38:01 +07:00
simon
2d6cc43a9e better label placement on toggle box 2021-12-18 18:22:57 +07:00
simon
bf0b07f3d7 fix error when restarting player with same video 2021-12-18 17:15:53 +07:00
simon
88ebb6d648 fix autodelete error on empty download queue 2021-12-18 17:05:49 +07:00
simon
3015e59a29 fix styling of sub unsub frontent feedback with span tag 2021-12-18 16:56:32 +07:00
simon
7a3609e611 add unstable tag to sync_docker deploy 2021-12-18 16:35:01 +07:00
simon
ad8d75c3e8 add autodelete videos to ignore list, #112 2021-12-17 23:28:53 +07:00
simon
f36ae6c4b9 hotfix: explicitly set DJANGO_DEBUG env, #111 2021-12-17 23:02:38 +07:00
simon
8678cdcadb add missing favicon to login template 2021-12-17 23:00:30 +07:00
simon
1ac6da1f3e bumb django version 2021-12-17 18:45:32 +07:00
simon
e5b9b1caef remove now unused favicon 2021-12-17 18:32:46 +07:00
simon
8f80cf2d22 update roadmap 2021-12-17 18:29:25 +07:00
simon
5f3d52c76a update docs for v0.0.9 release 2021-12-17 16:59:52 +07:00
simon
816c68464b add unstable tag for docker deploy 2021-12-17 16:57:10 +07:00
simon
a500f9ec63 add notifications and error messages to settings page 2021-12-17 15:09:21 +07:00
simon
8e976922e9 add Scheduler Setup documentation 2021-12-17 11:52:32 +07:00
simon
eb5483c6b2 bumb es version 2021-12-17 09:38:44 +07:00
simon
3231dbe29a the favicons the whole favicons and nothing but the favicons, #93 2021-12-16 21:23:54 +07:00
simon
ae082eb309 add sync_redis_state before building schedule for update path 2021-12-16 21:22:05 +07:00
simon
23f9894154 move validate_thumbnails out of startup has it's own schedule now 2021-12-16 21:20:14 +07:00
simon
f6159a48d5 change sub and unsub to colored toggle button 2021-12-16 18:28:52 +07:00
simon
ada2c1fe59 add autofocus and autocomplete to user form field, #104 2021-12-16 18:00:36 +07:00
simon
5b59c3296f port overwrite with TA_PORT and TA_UWSGI_PORT for collisions, #103 2021-12-16 17:42:59 +07:00
simon
c9373eee15 add remember me checkbox to login view and set session expire, #77 2021-12-16 15:17:58 +07:00
simon
e858bba944 fix linting, update doc string 2021-12-16 12:48:12 +07:00
simon
9a2dead76c implement autodelete watched videos after x days, #56 2021-12-16 12:44:37 +07:00
simon
53f93a7bb9 fix missing average_rating due to lack of dislikes 2021-12-15 17:39:52 +07:00
simon
7924f28ccb implement backup rotate from config value 2021-12-14 19:40:46 +07:00
simon
ff2de43752 update index restore information 2021-12-14 19:21:07 +07:00
simon
275cf1263b fix mobile backup-grid-row layout 2021-12-14 19:15:29 +07:00
simon
f49895917a change settings for backup restore 2021-12-14 19:06:47 +07:00
simon
f7c73f7eba change backup and restore to tag files 2021-12-14 19:05:58 +07:00
simon
6ad1c8e73b add reason to backup as file name tag 2021-12-13 21:20:48 +07:00
simon
21f70493bb add scheduler config to regular redis config 2021-12-13 20:28:41 +07:00
simon
5cb6076a1c use config check_reindex_days for refresh_interval 2021-12-13 19:19:49 +07:00
Wesley Mauk
f660b05a20 Make copyright year dynamic (#107)
Never have to update this again!
2021-12-13 19:10:11 +07:00
simon
8f7179ad0c fix some spacing issues 2021-12-13 10:57:29 +07:00
simon
3df9a53a24 add scheduler settings with some sane defaults 2021-12-13 10:55:57 +07:00
simon
fde1a37563 add check_reindex_days and run_backup_rotate fields to form 2021-12-13 10:54:54 +07:00
simon
c475700cdd store beat schedule on volume 2021-12-08 16:15:25 +07:00
simon
9bb68aac66 extend theater mode to video id page template 2021-12-08 14:30:13 +07:00
simon
279c4538ca fix player wrapper and player channel links 2021-12-07 08:25:46 +07:00
simon
42eec604a7 reimagining the video player in theater mode, #98 2021-12-06 22:14:42 +07:00
simon
e2238c16f5 restructure html to allow for non boxed-content classes within content blocks, #98 2021-12-06 18:07:57 +07:00
simon
8c5bb0bc1f add missing mapping for playlist_last_refresh to fix empty index scan, #101 2021-12-06 16:56:56 +07:00
simon
5c26be43e6 skip instead of cancle update 2021-12-06 16:55:55 +07:00
simon
82a71ed75d clean up playlist subscribe notification 2021-12-05 22:35:46 +07:00
simon
7516090e14 handle findPlaylists notification and fix spacing 2021-12-05 22:15:10 +07:00
simon
8874168291 clean up progress hook title and speed up playlist validation 2021-12-05 21:57:33 +07:00
simon
19871d55e2 faster release message timer with reduced redis exipre time on last message 2021-12-05 21:27:43 +07:00
simon
c7d69b4fa1 use new notification for channel subscribe feedback 2021-12-05 20:26:39 +07:00
simon
5b47c9cde7 use filter to search for relevant messages 2021-12-05 20:26:17 +07:00
simon
67bde4c7ec fix notification channel for video thumb download 2021-12-05 17:24:57 +07:00
simon
a0d5837519 use new checkMessages for frontend button calls 2021-12-05 17:24:20 +07:00
simon
fbdbfd744d rewrite of progress.js, now potentially better and flexible 2021-12-05 16:42:33 +07:00
simon
1d262fc936 fix old status variables for message 2021-12-05 16:41:06 +07:00
simon
f7b5082983 use message:playlistscan for index_channel_playlists 2021-12-03 21:05:34 +07:00
simon
b35440253d transform add to queue, rescan, and download progress to channel messages 2021-12-03 19:47:43 +07:00
simon
1691bdadf5 transform progress message to list, change subscribe messages 2021-12-03 19:00:26 +07:00
simon
5a2d7c07bf variable expire value and simpler REDIS_PORT declaration 2021-12-03 13:51:34 +07:00
simon
3d0bb40da6 for some strange reason timezone needs to be defined here again... 2021-12-03 12:00:40 +07:00
simon
0b0634273a validate form entries, set auto sensible default 2021-12-03 11:27:26 +07:00
simon
ab8dc0ddee bump versions 2021-12-03 11:26:35 +07:00
simon
4136a2a4f2 add rescan lock 2021-12-02 19:11:45 +07:00
simon
40c7a6a3f7 initial scheduler_form for settings 2021-12-02 18:38:32 +07:00
simon
b2861e0369 ScheduleBuilder config class for celery beat 2021-12-02 18:37:54 +07:00
simon
012e28d4ef initial beat scheduler setup 2021-12-02 15:54:29 +07:00
simon
3cc5db7ad1 use TZ environ for scheduler 2021-12-02 14:44:37 +07:00
simon
7e97284374 auto index playlists when adding to download 2021-11-29 15:49:45 +07:00
simon
b8359a4249 match_all to show all playlists, even when empty 2021-11-29 14:28:23 +07:00
simon
b14a820a4c add 18 char len to valid playlists 2021-11-29 14:21:19 +07:00
simon
929311a943 merge roadmap edits into testing 2021-11-28 19:16:16 +07:00
Simon
c754badbaa Update roadmap 2021-11-28 19:13:29 +07:00
simon
eacfd4bc84 refactor and move frontend PostData class to dedicated module 2021-11-28 18:42:04 +07:00
simon
78b85fd42f add note about channel playlists 2021-11-27 17:39:18 +07:00
simon
525618a7a1 update footer to v0.0.8 2021-11-27 14:41:10 +07:00
simon
436196738e remove untested warning of arm64, specify broser codec limitations 2021-11-27 14:40:03 +07:00
simon
d95bf4272c speedup by only testing selected formats 2021-11-27 14:13:48 +07:00
simon
5401722d88 bump versions 2021-11-27 14:13:10 +07:00
simon
2d69f64121 fix some wording 2021-11-27 14:11:06 +07:00
simon
b8f7aa3298 cleaner handeling of findPlaylists 2021-11-27 12:07:27 +07:00
simon
64c9269ee2 fix channel info for playlistid view to use actual owner 2021-11-27 11:55:00 +07:00
simon
295ea0cde0 handling deactivating playlist 2021-11-27 11:47:12 +07:00
simon
1f82f0c40d index playlist channel if doesn't exist and beter user feedback 2021-11-26 17:34:59 +07:00
simon
31515961e1 fix prev and nex playlist order 2021-11-26 16:47:39 +07:00
simon
5d7a609285 skip backing up not yet existing index 2021-11-26 16:19:18 +07:00
simon
e3de9a0184 fix mobile playlist nav layout 2021-11-26 10:10:45 +07:00
simon
ee436ea2d5 update docs with playlist info 2021-11-25 23:38:00 +07:00
simon
32d2659658 handling genering index write errors, #91 2021-11-25 22:10:12 +07:00
simon
3f31e50b69 add delete download queue pending or ignored, #85 2021-11-25 20:02:25 +07:00
simon
408b0a0c34 fix mobile layout for playlist pages 2021-11-25 17:22:52 +07:00
simon
39304ff2bd add subscribe and unsubscribe buttons for channels and playlists, #81 2021-11-25 16:41:58 +07:00
simon
bad129d630 consolidate subscribe to single task for channel and playlist 2021-11-25 15:49:08 +07:00
simon
2ebd0a3e2f extend unsubscribe for channels and playlists 2021-11-25 11:52:14 +07:00
simon
407325e0a9 add original youtube link if available, #81 2021-11-25 11:32:51 +07:00
simon
e3f9bd98de use --check-formats for downloader, #90 2021-11-25 11:17:25 +07:00
simon
b1783d6be3 handle multifeed videos with noplaylist 2021-11-22 22:58:25 +07:00
simon
9fceb98f13 add playlists to reindex class 2021-11-22 20:40:11 +07:00
simon
220d020c76 delete playlist buttons to frontend 2021-11-20 18:27:10 +07:00
simon
b292a5e309 add delete playlist backend functionality 2021-11-20 14:32:36 +07:00
simon
8d708fcdc8 add mark_playlist_watched and refactor mark_channel_watched to update_by_query 2021-11-20 10:58:25 +07:00
simon
85d12d6c68 dont index playlists without videos 2021-11-19 16:57:07 +07:00
simon
fef26a06b2 force refresh for new videos to avoid racing condition 2021-11-19 16:26:05 +07:00
simon
3b6e70b4dc progress message on validate_playlists 2021-11-19 15:53:06 +07:00
simon
3881d305f9 add subscribe to playlist form and better task management 2021-11-19 11:52:27 +07:00
simon
fcddf85245 validate_playlists after run_queue from VideoDownloader 2021-11-18 22:35:08 +07:00
simon
d0e0903d48 use remote data for playlist find_missing 2021-11-18 21:50:35 +07:00
simon
3efdc1962d fix playlist nav builder using correct index 2021-11-18 21:16:18 +07:00
simon
f26fbc0dc5 implement show_subed_only on playlist view 2021-11-18 17:14:11 +07:00
simon
a1167bac4e use same page size for playlist rescan 2021-11-18 16:58:39 +07:00
simon
bb889f7f67 use find_missing to also add missing videos from playlist to download queue 2021-11-18 16:37:05 +07:00
simon
1af0196208 add subscribe to playlist backend functionality 2021-11-18 16:00:55 +07:00
simon
8b69be645c use new IndexPaginate class for get_channels and get_playlists 2021-11-18 14:48:31 +07:00
simon
f371a03cc7 use new IndexPaginate class for get_all_indexed videos 2021-11-18 12:16:21 +07:00
simon
509b0097fe bump celery version 2021-11-18 12:11:58 +07:00
simon
7b36cc53e7 refactor get_all_pending to use new IndexPaginate class 2021-11-17 18:30:24 +07:00
simon
ed9d5aef0a fix playlist get_entries when not passing all youtube_ids 2021-11-17 18:16:22 +07:00
simon
c4b0f900f8 dedicated search_after class to scroll through index 2021-11-17 17:54:47 +07:00
simon
0e9c0d9f6b add channel playlist link 2021-11-15 13:19:40 +07:00
simon
d2414434fb implement playlist earch form 2021-11-15 13:08:32 +07:00
simon
2fe8936e85 add playlist position index 2021-11-15 13:00:17 +07:00
simon
62ba1ecf4a handle empty playlist nav 2021-11-14 19:48:03 +07:00
simon
e450b38702 add video playlist navigation, previous and next 2021-11-14 19:34:57 +07:00
simon
3d969c25fa only show playlist if at least one entry 2021-11-14 15:51:55 +07:00
simon
8932a6bc02 validate existence of playlist entry in index 2021-11-14 15:46:24 +07:00
simon
e3f0075132 use original sort order for playlist_id list 2021-11-13 18:36:48 +07:00
simon
b9ddf6a299 bump celery version 2021-11-13 17:54:07 +07:00
simon
c3ece970ee add playlist discovery button to channel_id page 2021-11-13 17:35:48 +07:00
simon
ac14cd5ebb playlist relation take 2 2021-11-13 17:34:58 +07:00
simon
7930da5242 fix youtu.be extractor, #40 2021-11-12 19:26:47 +07:00
simon
58303047aa validate thumbnails after filesystem rescan 2021-11-12 17:12:53 +07:00
simon
b1c82759bb extend playlist_id with metadata and links 2021-11-12 15:01:39 +07:00
simon
94576b4b76 initial PlaylistIdView paths and basic template 2021-11-12 11:44:18 +07:00
simon
16833a4377 auto add videos to playlist 2021-11-12 11:43:44 +07:00
simon
44e1720aab add thumbnail extraction to channel playlist scraper 2021-11-11 21:17:04 +07:00
simon
3485f9b1c8 define playlist field types in index 2021-11-11 20:57:28 +07:00
simon
c499a130da add sync playlist meta data and position to video index 2021-11-11 17:56:29 +07:00
simon
824ba35c14 add basic playlists to template 2021-11-10 18:53:38 +07:00
Crocs
bb73d531a4 Update settings.html (#87)
* Update settings.html

Fixed the example as you wouldn't put KB/s at the end, reworded the explanation, and added "KB/s" at the end of the "Current throttled rate limit:"

Also added KB/s to the download speed limit, and added an example.

* Update settings.html
2021-11-10 17:59:11 +07:00
simon
5ef9c5b059 download playlist artwork 2021-11-10 17:55:34 +07:00
simon
2550016ac8 send playlist hits to template 2021-11-10 17:29:11 +07:00
simon
eaf107d0d2 add new ta_playlist index and task to find playlists of channel 2021-11-08 14:54:17 +07:00
simon
1bfb912d09 basic routes and template for playlist page 2021-11-08 14:52:46 +07:00
simon
bd37b653c2 extend playlists to get all playlists of a channel and upload single playlist to index 2021-11-06 22:30:39 +07:00
simon
15845da3a8 use check_formats to validate thumb url, #83 2021-11-06 20:23:59 +07:00
simon
72bc6a4d02 basic methods of YoutubePlaylist class 2021-11-05 16:24:05 +07:00
simon
661ee2616c bump django version 2021-11-05 16:23:15 +07:00
simon
3bc6074ef3 slimming some UI spacing issues 2021-11-04 15:53:47 +07:00
simon
232afee3f6 update for v0.0.7 2021-11-01 17:16:00 +07:00
simon
b2a4f88980 add re_sync_thumbs to frontend 2021-11-01 16:42:07 +07:00
simon
1f0e305527 add re_sync_thumbs backend functionality 2021-11-01 16:20:21 +07:00
simon
4d9cda3a24 better subscribe_to task with notification 2021-10-31 17:48:56 +07:00
simon
283a72308e handle deleting channel without videos 2021-10-31 17:30:29 +07:00
simon
164ee5c523 more valueerrors in url extraction 2021-10-31 17:05:13 +07:00
simon
bce1022677 userspace channelID view_style 2021-10-31 16:15:33 +07:00
simon
1ba3090db1 rewrite url_str extractor to convert channel names into channel ids, #40 2021-10-31 16:04:28 +07:00
simon
c92800701a be more specific about multi-single user 2021-10-30 19:32:47 +07:00
simon
caff9ce189 standardize parse_url_list to only return ids 2021-10-30 17:51:50 +07:00
simon
3375cbc767 cleanup logging 2021-10-30 17:51:07 +07:00
simon
cc90e437a9 show progress message for thumbnail download 2021-10-30 17:06:54 +07:00
simon
29fc5fbda2 improved progress message for frontend feedback 2021-10-30 17:06:22 +07:00
simon
20aab04fbf fix keyerror 2021-10-30 16:35:10 +07:00
simon
45c2215dc2 validate subscribe form and handover to scheduler, #75 2021-10-30 15:00:10 +07:00
simon
aebf44ee7e validate add to queue form 2021-10-30 14:14:16 +07:00
simon
266d3703cd validate search forms 2021-10-30 13:19:16 +07:00
simon
a8ded25b35 userspace color 2021-10-29 23:43:19 +07:00
simon
a4397b5204 validate settings form and userspace archive page_size 2021-10-29 22:37:31 +07:00
simon
f24a3dd1f5 userspace sort_order and sort_by 2021-10-29 14:42:12 +07:00
simon
ef13b9259e userspace hide_watched in home and channelid view 2021-10-29 14:02:21 +07:00
simon
180a63d6dd userspace show_subed_only in channel view 2021-10-29 13:35:37 +07:00
simon
88f28a7d1e userspace show_ignored_only on downloads view 2021-10-29 12:23:33 +07:00
simon
b2ca7710cd userspace gird-list view 2021-10-29 11:45:13 +07:00
simon
21d55561a0 extend sort and filter to channel_id view 2021-10-28 18:00:59 +07:00
simon
8255fe9e55 validate expected env vars before starting 2021-10-28 16:17:47 +07:00
simon
254c518505 basic auth for elasticsearch 2021-10-28 15:49:58 +07:00
simon
ebd8368856 namespace redis keys to ta: 2021-10-27 18:07:35 +07:00
simon
78561981ad extend sort_by and sort_order in home view 2021-10-27 15:29:17 +07:00
simon
d4e266f9c0 refactor home view to view_config dict 2021-10-27 11:01:09 +07:00
simon
bb909d866c consolidating elasticsearch details 2021-10-26 18:38:43 +07:00
simon
969b6bff0d add throttled-rate and user management docs 2021-10-26 18:21:36 +07:00
simon
5f3fc460c1 implementing throttled-rate option 2021-10-26 17:14:26 +07:00
simon
581314a8a4 bump yt_dlp version 2021-10-26 17:07:52 +07:00
simon
50396a666d use expose instead of publish 2021-10-26 16:23:41 +07:00
simon
3526d62540 custom user models for admin login 2021-10-25 19:28:05 +07:00
simon
6f58dc47cc add admin interface link 2021-10-25 14:41:21 +07:00
simon
1283f437c3 use admin pw hash as secret_key 2021-10-25 14:22:23 +07:00
simon
247c0845e0 put db on persistend volume 2021-10-25 13:40:09 +07:00
simon
66f184b41b increase timeout to wait for es 2021-10-25 13:23:54 +07:00
simon
0ce5183056 handle failed login 2021-10-24 15:34:00 +07:00
simon
dffe974f9e move redundant keywords from downlaod title to badge, #64 2021-10-22 18:24:18 +07:00
simon
05b25d004e fix linting 2021-10-22 18:23:06 +07:00
simon
825ebd874e handle next url parameter in login 2021-10-22 12:01:30 +07:00
simon
01ccca16e4 added logout functionality 2021-10-22 11:45:06 +07:00
simon
3d0859ceec add basic usermanagement and login functionality 2021-10-21 19:33:27 +07:00
simon
401aa6c2f8 Merge branch 'master' into testing 2021-10-21 11:24:26 +07:00
simon
aeb6f48ac4 add iOS compatible format example to settings page 2021-10-21 11:15:38 +07:00
borgmanJeremy
d03c45e580 Updated readme to specify how to operate with iOS and MacOS. (#66) issue #61 2021-10-21 11:13:39 +07:00
simon
e4352e5bec playsinline fix for mobile safari 2021-10-20 20:49:20 +07:00
simon
58efe64c5d make chown command optional by omitting HOST_UID and HOST_GID, #58 2021-10-20 18:41:39 +07:00
simon
6f4bab41d5 increase uwsgi buffer-size, #60 #48 2021-10-20 18:16:53 +07:00
simon
d250ed645a initial template for login route 2021-10-18 17:14:59 +07:00
Crocs
09584e43b8 Update README.md (#57)
Unraid installation instructions
2021-10-18 10:20:59 +07:00
simon
4ef98b7448 simplify docker deployment 2021-10-17 13:29:22 +07:00
simon
528938942a update docs for v0.0.6 take 2 2021-10-17 11:31:21 +07:00
simon
de2371c43c handle running empty dl queue, #55 2021-10-17 11:28:05 +07:00
simon
1a88c85436 skip video if is_live now 2021-10-17 10:49:02 +07:00
simon
63be6afcfe allow duration_sec extraction to fail cleanly for add to queue 2021-10-17 09:40:46 +07:00
simon
559a4c9a58 a note about multi arch builds 2021-10-16 18:12:41 +07:00
simon
08338f3270 add thumbnail download for manual import 2021-10-16 17:03:39 +07:00
simon
0c3c1b650a disable notifiation on rescan, fix keyerror issue in missing_thumbs build 2021-10-15 13:16:35 +07:00
simon
45d8b7e218 ignore missing video rating 2021-10-14 23:12:30 +07:00
simon
5edc78a826 update docs for v0.0.6 2021-10-14 22:48:43 +07:00
simon
fd2fa628e3 make sure to get both banner and icon 2021-10-14 22:48:23 +07:00
simon
ba12c2ad25 simple progress message for thumbnail bulk download 2021-10-14 21:28:27 +07:00
simon
0d061d8601 get art work for new subscribed channels 2021-10-14 17:47:07 +07:00
simon
0844f7b91a add delayed retry for non 404 thumb download error 2021-10-14 14:44:50 +07:00
simon
044b12d5a5 remove now unused thumbnail methods from searching module 2021-10-14 14:42:35 +07:00
simon
ce5f8b6a38 cleanup method for unused thumbs 2021-10-14 11:59:30 +07:00
simon
9ad42dfa38 add artwork refresh on rescan 2021-10-14 11:18:32 +07:00
simon
952bc4f97c add thumb dl after rescan 2021-10-14 10:03:54 +07:00
simon
bfd51e6804 extend test deploy to take platform argument 2021-10-13 19:34:55 +07:00
simon
72af95acd8 fallback channel and video artwork for 404 errors 2021-10-13 19:34:16 +07:00
simon
3473c79d63 use TARGETPLATFORM to install repo ffmpeg for arm64 and patched ffmpeg for amd64 builds 2021-10-12 23:00:25 +07:00
simon
a4ae6ca1ca Merge branch 'testing' of github.com:bbilly1/tubearchivist into testing 2021-10-12 17:49:18 +07:00
lamusmaser
48a4827ad5 Update deploy.sh to utilize buildx for multiarch builds. This is inline with Issue #6. (#51) 2021-10-12 17:47:48 +07:00
simon
29f17680bf consolidate delete thumbnails into ThumbManager class 2021-10-11 19:29:27 +07:00
simon
519c710e7d testing tests 2021-10-11 16:55:47 +07:00
simon
1b04c00ae7 standardizing on progress/ url 2021-10-11 16:55:08 +07:00
simon
9f12966ef7 bumping yt_dlp version, requires additional noprogress option 2021-10-11 16:34:19 +07:00
simon
76f49b4cf7 comparing with instance instead 2021-10-11 16:11:30 +07:00
simon
35a5eda36e downloading channel artwork after scraping 2021-10-11 16:03:25 +07:00
simon
567e9d473b fix killing task with missing task id 2021-10-11 15:54:36 +07:00
simon
e202265a5f extended validate_thumbnails for channel art 2021-10-11 14:27:48 +07:00
simon
bc84696792 new thumbnails module and new cache layout 2021-10-11 13:33:28 +07:00
simon
f5621954fb star icons for video rating 2021-10-10 16:09:02 +07:00
simon
ef75f6dd16 add delete channel to frontend 2021-10-09 20:33:32 +07:00
simon
b53a1763c8 better channel folder path builder with clean_string 2021-10-09 20:32:42 +07:00
simon
11780a0ff0 add delete channel backend 2021-10-09 19:54:36 +07:00
simon
8d3e2f14fc add delete video to frontend 2021-10-09 17:11:13 +07:00
simon
7405e960e5 delete video backend functionality 2021-10-08 16:18:01 +07:00
simon
8c97e6786a chown to fix permissions on new channel folders 2021-10-08 15:16:02 +07:00
simon
563222a26e fix linting 2021-10-08 15:10:44 +07:00
simon
64ccd3830e implementing filesystem rescan to clean index 2021-10-08 14:56:07 +07:00
simon
c2a6ac6f44 fix wiki link 2021-10-08 11:36:18 +07:00
simon
0b88fd8b1f implement watched-unwatched toggle, #39 2021-10-07 23:38:17 +07:00
simon
744780f4bd add optional EmbedThumbnail postprocessor 2021-10-07 22:28:32 +07:00
simon
4d152e1e54 bumping versions 2021-10-07 20:08:38 +07:00
simon
3500044b0d adding woff files and OFL license 2021-10-07 19:41:52 +07:00
simon
7e0abc0d20 fix run sync_redis_state on startup 2021-10-04 01:07:15 +07:00
simon
b0fb2bbb00 update roadmap 2021-10-03 20:25:35 +07:00
simon
4e7cba70d5 update to new ffmpeg naming convention 2021-10-03 20:25:23 +07:00
simon
28216e455e bumping versions for new release 2021-10-03 19:35:33 +07:00
simon
7df4233224 cleaning up leftover code from old vesions 2021-10-03 19:33:58 +07:00
simon
5c2a8286d3 cleanup and fixing some spacing issues 2021-10-03 18:54:04 +07:00
simon
f5f3617e96 implement toggle view to hide watched videos 2021-10-03 18:52:29 +07:00
simon
ccc8a658b7 implement toggle view to show subscribed only 2021-10-03 18:44:44 +07:00
simon
ed73bff8fa implement toggle view ignored only in downloads page 2021-10-03 18:17:07 +07:00
simon
26a0f8930a renaming 'download queue' button to 'start download' 2021-10-03 12:25:56 +07:00
simon
30d95315b2 consolidate documentation in wiki 2021-10-03 12:05:39 +07:00
simon
c95c16208d removed documentation with links to wiki and more 2021-10-03 11:47:29 +07:00
simon
18459e2486 about closing issues 2021-10-03 11:24:31 +07:00
simon
280e83bb2c kibana for testing ES queries 2021-10-02 19:48:17 +07:00
simon
95cdcbae9a grid and list view for channels overview page 2021-10-02 19:39:02 +07:00
simon
0b0502e246 grid and list view for downloads template 2021-10-02 18:37:22 +07:00
simon
99781290db grid and list view for home and channel id templates 2021-10-01 16:24:39 +07:00
simon
7c34ceb9f8 framework to switch between grid and list view 2021-10-01 14:42:17 +07:00
simon
21ad1fd832 use latest tag instead 2021-10-01 13:44:14 +07:00
simon
b12df3f312 using ffmpeg patched builds instead of repo version, #37 #26 2021-09-30 22:01:30 +07:00
simon
4d348955a3 allowing custom redis port 2021-09-30 18:03:23 +07:00
simon
2b89408ec1 ignore wiki from container 2021-09-30 17:58:56 +07:00
simon
e52becd50e a word about setting up your testing environment 2021-09-30 14:21:48 +07:00
simon
2b6a805c1d fix links and icons 2021-09-29 13:19:21 +07:00
simon
108b1bac1f fix links 2021-09-28 22:09:58 +07:00
simon
12ad8fcead documenting main pages 2021-09-28 21:58:43 +07:00
simon
2905a67ab9 show ignored videos, forget or download ids 2021-09-28 16:53:45 +07:00
simon
f5a56fca86 refactored redis functions into dedicated RedisArchivist class 2021-09-28 10:33:00 +07:00
simon
a242f744d5 moove startup functions to ready() method to avoid double execs 2021-09-27 17:45:44 +07:00
simon
3d79f08311 handle return value of del_message 2021-09-27 17:44:42 +07:00
simon
f22e02d2f1 hotfix to release leftover locks on restart 2021-09-27 10:38:57 +07:00
simon
9528e11ca2 updated roadmap 2021-09-26 12:59:58 +07:00
simon
85eaac0b57 bumping versions for new release 2021-09-26 12:05:41 +07:00
simon
fab918db79 setting limit_count default to false after implementing dynamic dl queue 2021-09-26 12:01:51 +07:00
simon
72e45bcf5a restructured, added comment about updating and donating 2021-09-26 11:34:54 +07:00
simon
ea2d0bcb6c initial wiki pages 2021-09-26 11:21:54 +07:00
simon
11067094b2 implement os.listdir sanitizer for hidden files, #30 2021-09-25 18:59:54 +07:00
simon
2de99d7e37 better implementation for dl icon directly in message builder 2021-09-25 18:54:37 +07:00
simon
c165f152a9 allowing to cancle download_now tasks 2021-09-25 17:40:33 +07:00
simon
51ec765433 add kill queue function to frontend 2021-09-25 15:35:36 +07:00
simon
47020e0cfa implement kill function for dl queue 2021-09-24 23:37:26 +07:00
simon
f53391c1bb add limit_queue setting back, make buttons only show up while downlaoding 2021-09-24 21:27:53 +07:00
simon
d0b54f8a88 add stop queue button to frontend 2021-09-24 18:03:22 +07:00
simon
91a57cc780 fix duplication issue with download_now 2021-09-24 16:58:48 +07:00
simon
fb1913f912 ignore now also removes itself from redis queue 2021-09-24 16:58:05 +07:00
simon
ea6d81102f a word about updating tube archivist 2021-09-23 22:40:38 +07:00
simon
8fc5926ce7 rewrote download_single and download_pending tasks for redis queue 2021-09-23 18:10:45 +07:00
simon
78d8bc9d24 changed dl queue to redis, changed sort order to represent actual queue order 2021-09-23 18:09:46 +07:00
simon
7f57cabbc6 initial dynamic dl queue in redis 2021-09-23 16:58:47 +07:00
simon
214a248821 better error handeling in add to download form 2021-09-23 10:50:44 +07:00
simon
ef3447cbfb fix read waiting for return issue 2021-09-22 18:11:05 +07:00
simon
80fcbddd65 typo... 2021-09-22 14:58:50 +07:00
simon
65619ac466 updated roadmap and section about backup-restore 2021-09-22 14:56:29 +07:00
simon
5d519ebda3 added section about formatting and linting 2021-09-22 12:18:54 +07:00
simon
023b5a4aba Reducing max-complexity to 12 after code refactor #25
* Added helper function in deploy.sh to run same tests as github actions locally
2021-09-22 12:03:52 +07:00
Christian Clauss
fd50058431 GitHub Actions: Make black, flake8, and isort mandatory tests (#25)
* GitHub Actions: Make black, flake8, and isort mandatory tests

* fixup! black --diff
2021-09-22 12:00:33 +07:00
simon
ecae40d502 refactor PostData class
* Split up into SearchForm class as part of searching module
* Split up into WatchState class as part of index module
2021-09-22 11:43:38 +07:00
simon
aa1b9f4d3b pending refactor 2021-09-21 16:53:05 +07:00
simon
f8222855f0 customizing linting settings 2021-09-21 16:46:18 +07:00
simon
9ec9bf83c7 adding github action and unused import fix 2021-09-21 16:25:33 +07:00
simon
2433e0e7d8 linting everything in black 2021-09-21 16:25:22 +07:00
Christian Clauss
49e57955b5 GitHub Action to lint Python code (#14)
* GitHub Action to lint Python code
2021-09-21 16:23:36 +07:00
Daniel Kempkens
69e6e490f4 Optionally embed metadata into video files (#21)
This adds basic `postprocessors` support and adds a setting to
optionally enable `FFmpegMetadata`.

It's the same as passing `--add-metadata` and `--add-chapters` via the
`yt-dlp` CLI.
2021-09-21 10:12:33 +07:00
simon
1c3ca8364c added restore_from_backup to frontend and task 2021-09-20 20:26:28 +07:00
simon
c17e2e6dd1 skip post_bulk_restore if empty 2021-09-20 19:10:39 +07:00
simon
4b9acd4c51 load new defaults from config.json #21 2021-09-20 17:45:01 +07:00
simon
690eb5befd added section about releases, master vs testing branch, #22 2021-09-20 09:56:50 +07:00
TechnicallyOffbeat
978fa6a313 Added CONTRIBUTING.md (#22)
Basic CONTRIBUTING.md file.
2021-09-20 09:43:31 +07:00
TechnicallyOffbeat
2d2f60fddf settings.html spelling and grammar (#16)
* Grammar and spelling for setting.html

Fixed grammatical and spelling errors
2021-09-19 09:51:22 +07:00
simon
ef64100d8f flake8 and isort linting 2021-09-18 20:02:54 +07:00
simon
d2d6835d53 automating versioning and pushing images to docker 2021-09-18 17:36:04 +07:00
simon
cbedfe2acb running codespell #15 2021-09-18 17:28:16 +07:00
simon
eafda731c9 restoring indexes from zip archive 2021-09-18 17:10:16 +07:00
TechnicallyOffbeat
82e4ee0350 Fixed punctuation, spelling, and grammar in about.html (#13)
* about.html spelling and grammar fixes

Fixed spelling and grammatical errors

* Fixed capitalization

Fixed capitalizing a "url" from the first commit.
2021-09-18 09:29:10 +07:00
simon
f9c09d215e sorting on _id is depreciated, using youtube_id instead 2021-09-17 12:52:00 +07:00
Simon
ea8f259553 Merge pull request #12 from TechnicallyOffbeat/master
README.md grammar and spelling
2021-09-17 12:44:54 +07:00
Simon
0306f4d4fc Merge branch 'master' into master 2021-09-17 12:43:24 +07:00
simon
3d2d11ba3a fix blocking issue where download_now wouldn't delay 2021-09-17 10:52:59 +07:00
TechnicallyOffbeat
184239947e Readme grammar and spelling
Updated grammar, spelling and punctuation
2021-09-16 21:18:03 -04:00
simon
46487d682e rewrite of DownloadView to allow pagination #7 2021-09-16 22:01:30 +07:00
simon
9b493ae9b0 ignore static files when already created 2021-09-16 21:59:36 +07:00
simon
c657baca97 backup functionality to frontend and tasks 2021-09-16 18:16:09 +07:00
simon
e53cfc5a32 backup es db to zip file
backup es db to zip file
2021-09-16 17:47:01 +07:00
simon
9234164ff6 add a section about elasticsearch permission error and english is hard 2021-09-16 15:59:20 +07:00
simon
947538815d expose should be 8000 #9 2021-09-16 15:36:14 +07:00
214 changed files with 20578 additions and 4290 deletions

View File

@@ -17,5 +17,8 @@ venv/
# Unneeded graphics
assets/*
# Unneeded docs
docs/*
# for local testing only
testing.sh

17
.eslintrc.js Normal file
View File

@@ -0,0 +1,17 @@
'use strict';
module.exports = {
extends: ['eslint:recommended', 'eslint-config-prettier'],
parserOptions: {
ecmaVersion: 2020,
},
env: {
browser: true,
},
rules: {
strict: ['error', 'global'],
'no-unused-vars': ['error', { vars: 'local' }],
eqeqeq: ['error', 'always', { null: 'ignore' }],
curly: ['error', 'multi-line'],
'no-var': 'error',
},
};

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

@@ -0,0 +1,3 @@
github: bbilly1
ko_fi: bbilly1
custom: https://paypal.me/bbilly1

55
.github/ISSUE_TEMPLATE/BUG-REPORT.yml vendored Normal file
View File

@@ -0,0 +1,55 @@
name: Bug Report
description: File a bug report
title: "[Bug]: "
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to help improve this project!
- type: checkboxes
id: latest
attributes:
label: "I've read the documentation"
options:
- label: I'm running the latest version of Tube Archivist and have read the [release notes](https://github.com/tubearchivist/tubearchivist/releases/latest).
required: true
- label: I have read through the [wiki](https://github.com/tubearchivist/tubearchivist/wiki) and the [readme](https://github.com/tubearchivist/tubearchivist#installing-and-updating), particularly the [common errors](https://github.com/tubearchivist/tubearchivist#common-errors) section.
required: true
- type: input
id: os
attributes:
label: Operating System
description: On what operating system are you running Tube Archivist on?
placeholder: ex. Linux, Unraid, Synology...
validations:
required: true
- type: textarea
id: what-happened
attributes:
label: Your Bug Report
value: "## Describe the bug\n\n## Steps To Reproduce\n\n## Expected behavior"
validations:
required: true
- type: textarea
id: logs
attributes:
label: Relevant log output
description: Please copy and paste any relevant Docker logs. This will be automatically formatted into code, so no need for backticks.
render: shell
validations:
required: true
- type: textarea
attributes:
label: Anything else?
description: |
Links? References? Anything that will give us more context about the issue you are encountering!
Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in.
validations:
required: false

View File

@@ -0,0 +1,37 @@
name: Feature Request
description: I want to work on a new feature.
title: "[Feature Request]: "
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to help improve this project! This project is currently only taking new feature requests from contributors who want to make a PR with their ideas. The backlog of planned features outlined in the roadmap or the open issues here will keep us busy for the foreseeable future.
- type: checkboxes
id: already
attributes:
label: Already implemented?
options:
- label: I have read through the [wiki](https://github.com/tubearchivist/tubearchivist/wiki).
required: true
- label: I understand the [scope](https://github.com/tubearchivist/tubearchivist/wiki/FAQ) of this project and am aware of the [known limitations](https://github.com/tubearchivist/tubearchivist#known-limitations) and my idea is not already on the [roadmap](https://github.com/tubearchivist/tubearchivist#roadmap).
required: true
- type: textarea
id: description
attributes:
label: Your Feature Request
value: "## Is your feature request related to a problem? Please describe.\n\n## Describe the solution you'd like\n\n## Additional context"
placeholder: Tell us what you see!
validations:
required: true
- type: checkboxes
id: help
attributes:
label: Your help is needed!
description: This project is ambitious as it is, please contribute.
options:
- label: Yes I will work on this in the next few days or weeks.
required: true

View File

@@ -0,0 +1,45 @@
name: Installation Help
description: There is an issue with installing or configuring Tube Archivist
title: "[Installation]: "
body:
- type: markdown
attributes:
value: |
This is most likely not the best place to ask for installation help. There is a helpful community on [Discord](https://github.com/tubearchivist/tubearchivist#connect).
- type: checkboxes
id: instructions
attributes:
label: Installation instructions
options:
- label: I have read and understand the [installation instructions](https://github.com/tubearchivist/tubearchivist#installing-and-updating).
required: true
- label: My issue is not described in the [common errors](https://github.com/tubearchivist/tubearchivist#common-errors) section.
required: true
- type: input
id: os
attributes:
label: Operating System
description: On what operating system do you want to install Tube Archivist on?
placeholder: ex. Linux, Unraid, Synology...
validations:
required: true
- type: textarea
id: description
attributes:
label: Your installation or setup question
placeholder: Describe your issue
validations:
required: true
- type: textarea
id: logs
attributes:
label: Relevant log output
description: Please copy and paste any relevant Docker logs. This will be automatically formatted into code, so no need for backticks.
render: shell
validations:
required: true

16
.github/workflows/lint_js.yml vendored Normal file
View File

@@ -0,0 +1,16 @@
name: lint_js
on: [pull_request, push]
jobs:
check:
name: lint_js
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v3
with:
node-version: '16'
- run: npm ci
- run: npm run lint
- run: npm run format -- --check

27
.github/workflows/lint_python.yml vendored Normal file
View File

@@ -0,0 +1,27 @@
name: lint_python
on: [pull_request, push]
jobs:
lint_python:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: '3.10'
# note: this logic is duplicated in the `validate` function in ./deploy.sh
# if you update this file, you should update that as well
- run: pip install --upgrade pip wheel
- run: pip install bandit black codespell flake8 flake8-bugbear
flake8-comprehensions isort requests
- run: ./deploy.sh validate
# - run: black --check --diff --line-length 79 .
# - run: codespell --skip="./.git,./package.json,./package-lock.json,./node_modules"
# - run: flake8 . --count --max-complexity=10 --max-line-length=79
# --show-source --statistics
# - run: isort --check-only --line-length 79 --profile black .
# - run: pip install -r tubearchivist/requirements.txt
# - run: mkdir --parents --verbose .mypy_cache
# - run: mypy --ignore-missing-imports --install-types --non-interactive .
# - run: python3 tubearchivist/manage.py test || true
# - run: shopt -s globstar && pyupgrade --py36-plus **/*.py || true
# - run: safety check

7
.gitignore vendored
View File

@@ -4,5 +4,8 @@ __pycache__
# django testing db
db.sqlite3
# frontend fonts
*ttf.woff
# vscode custom conf
.vscode
# JavaScript stuff
node_modules

View File

@@ -1,5 +0,0 @@
{
"python.linting.pylintEnabled": true,
"python.linting.pycodestyleEnabled": false,
"python.linting.enabled": true
}

48
ARCHITECTURE.md Normal file
View File

@@ -0,0 +1,48 @@
# The Inner Workings of Tube Archivist
This is a high level overview of the architecture of Tube Archivist, intended for interested contributors to find your way around quickly.
```
Tube Archivist
______________________|_____________________
| | |
------------------- --------------- -------------------
| | | | | |
| DjangoProject | | RedisJson | | ElasticSearch |
| | | | | |
------------------- --------------- -------------------
```
## DjangoProject
This is the main Python application. Django serves its data container internally with **Uwsgi** on port 8080, the interface is served with **Nginx** on the public port 8000.
Users created static files like media files and artwork as well as application artwork like logos and fonts are served directly from Nginx, while the rest of the application uses uwsgi_pass to proxy the requests to uwsgi.
Config files are located in the `docker_assets` folder. The script `run.sh` is the container `CMD` command and entry point, validating env vars, connection to ElasticSearch (ES) and will start the application.
Compared to other Django projects, this application doesn't make use of the database models, due to a lack of integration with ES. This project has its own abstractions and integrations, treating ES as a REST API.
Long running application tasks are handed off to **Celery** - using **Redis** as a broker - to run asynchronously from the main threads.
- All tasks are defined in the `home.tasks.py` module.
There are three Django apps:
- **config**: The root app, routing the main endpoints and the main `settings.py` file
- **api**: The API app with its views and functionality
- **home**: Most of the application logic, templates and views, will probably get split up further in the future.
The *home* app is split up into packages in the `src` directory:
- **download**: All download related classes, interact with yt-dlp, download artwork, handle the download queue and post processing tasks.
- **es**: All index setup and validation classes, handles mapping validations and makes mapping changes, wrapper functions to simplify interactions with Elasticsearch, backup and restore.
- **frontend**: All direct interactions with the frontend, like Django forms, searching, watched state changes, and legacy api_calls in the process of moving to the api app.
- **index**: Contains all functionality for scraping and indexing videos, channels, playlists, comments, subtitles, etc...
- **ta**: Loose collection of functions and classes, handle application config and contains redis wrapper classes.
## RedisJson
Holds the main application config json object that gets dynamically edited from the frontend, serves as a message broker for **Celery**. Redis serves as a temporary and thread safe link between Django and the frontend, storing progress messages and temporary queues for processing. Used to store locking keys for threads and execution details for tasks.
- Wrapper classes to interact with Redis are located in the `home.src.ta.ta_redis.py` module.
## ElasticSearch (ES)
Is used to store and index all metadata, functions as an application database and makes it all searchable. The mapping defines which fields are indexed as searchable text fields and which fields are used for match filtering.
- The index setup and validation is handled in the `home.src.es.index_setup.py` module.
- Wrapper classes for making requests to ES are located in the `home.src.es.connect.py` module.

100
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,100 @@
## Contributing to Tube Archivist
Welcome, and thanks for showing interest in improving Tube Archivist!
If you haven't already, the best place to start is the README. This will give you an overview on what the project is all about.
## Report a bug
If you notice something is not working as expected, check to see if it has been previously reported in the [open issues](https://github.com/tubearchivist/tubearchivist/issues).
If it has not yet been disclosed, go ahead and create an issue.
If the issue doesn't move forward due to a lack of response, I assume it's solved and will close it after some time to keep the list fresh.
## Wiki
The wiki is where all user functions are documented in detail. These pages are mirrored into the **docs** folder of the repo. This allows for pull requests and all other features like regular code. Make any changes there, and I'll sync them with the wiki tab.
## Development Environment
I have learned the hard way, that working on a dockerized application outside of docker is very error prone and in general not a good idea. So if you want to test your changes, it's best to run them in a docker testing environment. You might be able to run the application directly, but this document assumes you're using docker.
### Instructions
Set up docker on your development machine.
Clone this repository.
Functional changes should be made against the unstable `testing` branch, so check that branch out, then make a new branch for your work.
Edit the `docker-compose.yml` file and replace the [`image: bbilly1/tubearchivist` line](https://github.com/tubearchivist/tubearchivist/blob/4af12aee15620e330adf3624c984c3acf6d0ac8b/docker-compose.yml#L7) with `build: .`. Also make any other changes to the environment variables and so on necessary to run the application, just like you're launching the application as normal.
Run `docker compose up --build`. This will bring up the application. Kill it with `ctrl-c` or by running `docker compose down` from a new terminal window in the same directory.
Make your changes locally and re-run `docker compose up --build`. The `Dockerfile` is structured in a way that the actual application code is in the last layer so rebuilding the image with only code changes utilizes the build cache for everything else and will just take a few seconds.
### Develop environment inside a VM
You may find it nice to run everything inside of a VM, though this is not necessary. There's a `deploy.sh` script which has some helpers for this use case. YMMV, this is what one of the developers does:
- Clone the repo, work on it with your favorite code editor in your local filesystem. *testing* branch is the where all the changes are happening, might be unstable and is WIP.
- Then I have a VM running standard Ubuntu Server LTS with docker installed. The VM keeps my projects separate and offers convenient snapshot functionality. The VM also offers ways to simulate lowend environments by limiting CPU cores and memory. You can use this [Ansible Docker Ubuntu](https://github.com/bbilly1/ansible-playbooks) playbook to get started quickly. But you could also just run docker on your host system.
- I have my local DNS resolve `tubearchivist.local` to the IP of the VM for convenience. To deploy the latest changes and rebuild the application to the testing VM run:
```bash
./deploy.sh test
```
- The command above will call the docker build command with `--build-arg INSTALL_DEBUG=1` to install additional useful debug tools.
- The `test` argument takes another optional argument to build for a specific architecture valid options are: `amd64`, `arm64` and `multi`, default is `amd64`.
- This `deploy.sh` script is not meant to be universally usable for every possible environment but could serve as an idea on how to automatically rebuild containers to test changes - customize to your liking.
## Working with Elasticsearch
Additionally to the required services as listed in the example docker-compose file, the **Dev Tools** of [Kibana](https://www.elastic.co/guide/en/kibana/current/docker.html) are invaluable for running and testing Elasticsearch queries.
**Quick start**
Generate your access token in Elasitcsearch:
```bash
bin/elasticsearch-service-tokens create elastic/kibana kibana
```
Example docker compose, use same version as for Elasticsearch:
```yml
kibana:
image: docker.elastic.co/kibana/kibana:0.0.0
container_name: kibana
environment:
- "ELASTICSEARCH_HOSTS=http://archivist-es:9200"
- "ELASTICSEARCH_SERVICEACCOUNTTOKEN=<your-token-here>"
ports:
- "5601:5601"
```
If you want to run queries on the Elasticsearch container directly from your host with for example `curl` or something like *postman*, you might want to **publish** the port 9200 instead of just **exposing** it.
## Implementing a new feature
Do you see anything on the roadmap that you would like to take a closer look at but you are not sure, what's the best way to tackle that? Or anything not on there yet you'd like to implement but are not sure how? Reach out on Discord and we'll look into it together.
## Making changes
To fix a bug or implement a feature, fork the repository and make all changes to the testing branch. When ready, create a pull request.
## Making changes to the JavaScript
The JavaScript does not require any build step; you just edit the files directly. However, there is config for eslint and prettier (a linter and formatter respectively); their use is recommended but not required. To use them, install `node`, run `npm i` from the root directory of this repository to install dependencies, then run `npm run lint` and `npm run format` to run eslint and prettier respectively.
## Releases
There are three different docker tags:
- **latest**: As the name implies is the latest multiarch release for regular usage.
- **unstable**: Intermediate amd64 builds for quick testing and improved collaboration. Don't mix with a *latest* installation, for your testing environment only. This is untested and WIP and will have breaking changes between commits that might require a reset to resolve.
- **semantic versioning**: There will be a handful named version tags that will also have a matching release and tag on github.
If you want to see what's in your container, checkout the matching release tag. A merge to **master** usually means a *latest* or *unstable* release. If you want to preview changes in your testing environment, pull the *unstable* tag or clone the repository and build the docker container with the Dockerfile from the **testing** branch.
## Code formatting and linting
To keep things clean and consistent for everybody, there is a github action setup to lint and check the changes. You can test your code locally first if you want. For example if you made changes in the **video** module, run
```shell
./deploy.sh validate tubearchivist/home/src/index/video.py
```
to validate your changes. If you omit the path, all the project files will get checked. This is subject to change as the codebase improves.

View File

@@ -1,32 +1,73 @@
# build the tube archivist image from default python slim image
# multi stage to build tube archivist
# first stage to build python wheel, copy into final image
FROM python:3.9.7-slim-bullseye
# First stage to build python wheel
FROM python:3.10.9-slim-bullseye AS builder
ARG TARGETPLATFORM
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential gcc libldap2-dev libsasl2-dev libssl-dev
# install requirements
COPY ./tubearchivist/requirements.txt /requirements.txt
RUN pip install --user -r requirements.txt
# build final image
FROM python:3.10.9-slim-bullseye as tubearchivist
ARG TARGETPLATFORM
ARG INSTALL_DEBUG
ENV PYTHONUNBUFFERED 1
# copy build requirements
COPY --from=builder /root/.local /root/.local
ENV PATH=/root/.local/bin:$PATH
# install distro packages needed
RUN apt-get clean && apt-get -y update && apt-get -y install --no-install-recommends \
build-essential \
ffmpeg \
nginx \
curl && rm -rf /var/lib/apt/lists/*
atomicparsley \
curl \
xz-utils && rm -rf /var/lib/apt/lists/*
# copy config files
COPY nginx.conf /etc/nginx/conf.d/
# install patched ffmpeg build, default to linux64
RUN if [ "$TARGETPLATFORM" = "linux/arm64" ] ; then \
curl -s https://api.github.com/repos/yt-dlp/FFmpeg-Builds/releases/latest \
| grep browser_download_url \
| grep ".*master.*linuxarm64.*tar.xz" \
| cut -d '"' -f 4 \
| xargs curl -L --output ffmpeg.tar.xz ; \
else \
curl -s https://api.github.com/repos/yt-dlp/FFmpeg-Builds/releases/latest \
| grep browser_download_url \
| grep ".*master.*linux64.*tar.xz" \
| cut -d '"' -f 4 \
| xargs curl -L --output ffmpeg.tar.xz ; \
fi && \
tar -xf ffmpeg.tar.xz --strip-components=2 --no-anchored -C /usr/bin/ "ffmpeg" && \
tar -xf ffmpeg.tar.xz --strip-components=2 --no-anchored -C /usr/bin/ "ffprobe" && \
rm ffmpeg.tar.xz
# install debug tools for testing environment
RUN if [ "$INSTALL_DEBUG" ] ; then \
apt-get -y update && apt-get -y install --no-install-recommends \
vim htop bmon net-tools iputils-ping procps \
&& pip install --user ipython \
; fi
# make folders
RUN mkdir /cache
RUN mkdir /youtube
RUN mkdir /app
RUN mkdir /cache /youtube /app
# install python dependencies
COPY ./tubearchivist/requirements.txt /requirements.txt
RUN pip install --no-cache-dir -r requirements.txt --src /usr/local/src
# copy config files
COPY docker_assets/nginx.conf /etc/nginx/sites-available/default
RUN sed -i 's/^user www\-data\;$/user root\;/' /etc/nginx/nginx.conf
# copy application into container
COPY ./tubearchivist /app
COPY ./run.sh /app
COPY ./uwsgi.ini /app
COPY ./docker_assets/run.sh /app
COPY ./docker_assets/uwsgi.ini /app
# volumes
VOLUME /cache
@@ -34,7 +75,7 @@ VOLUME /youtube
# start
WORKDIR /app
EXPOSE 80
EXPOSE 8000
RUN chmod +x ./run.sh

283
README.md
View File

@@ -1,15 +1,39 @@
![Tube Archivist](assets/tube-archivist-banner.jpg?raw=true "Tube Archivist Banner")
<center><h1>Your self hosted Youtube media server</h1></center>
<h1 align="center">Your self hosted YouTube media server</h1>
<div align="center">
<a href="https://github.com/bbilly1/tilefy" target="_blank"><img src="https://tiles.tilefy.me/t/tubearchivist-docker.png" alt="tubearchivist-docker" title="Tube Archivist Docker Pulls" height="50" width="190"/></a>
<a href="https://github.com/bbilly1/tilefy" target="_blank"><img src="https://tiles.tilefy.me/t/tubearchivist-github-star.png" alt="tubearchivist-github-star" title="Tube Archivist GitHub Stars" height="50" width="190"/></a>
<a href="https://github.com/bbilly1/tilefy" target="_blank"><img src="https://tiles.tilefy.me/t/tubearchivist-github-forks.png" alt="tubearchivist-github-forks" title="Tube Archivist GitHub Forks" height="50" width="190"/></a>
<a href="https://www.tubearchivist.com/discord" target="_blank"><img src="https://tiles.tilefy.me/t/tubearchivist-discord.png" alt="tubearchivist-discord" title="TA Discord Server Members" height="50" width="190"/></a>
</div>
## Table of contents:
* [Wiki](https://github.com/tubearchivist/tubearchivist/wiki) with [FAQ](https://github.com/tubearchivist/tubearchivist/wiki/FAQ)
* [Core functionality](#core-functionality)
* [Screenshots](#screenshots)
* [Problem Tube Archivist tries to solve](#problem-tube-archivist-tries-to-solve)
* [Connect](#connect)
* [Extended Universe](#extended-universe)
* [Installing and updating](#installing-and-updating)
* [Getting Started](#getting-started)
* [Common Errors](#common-errors)
* [Roadmap](#roadmap)
* [Known limitations](#known-limitations)
* [Donate](#donate)
------------------------
## Core functionality
* Subscribe to your favourite Youtube channels
* Subscribe to your favorite YouTube channels
* Download Videos using **yt-dlp**
* Index and make videos searchable
* Play videos
* Keep track of viewed and unviewed videos
## Tube Archivist on YouTube
[![ibracorp-youtube-video-thumb](assets/tube-archivist-ibracorp-O8H8Z01c0Ys.jpg)](https://www.youtube.com/watch?v=O8H8Z01c0Ys)
## Screenshots
![home screenshot](assets/tube-archivist-screenshot-home.png?raw=true "Tube Archivist Home")
*Home Page*
@@ -27,62 +51,145 @@
*Downloads Page*
## Problem Tube Archivist tries to solve
Once your Youtube video collection grows, it becomes hard to search and find a specific video. That's where Tube Archivist comes in: By indexing your video collection with metadata from Youtube, you can organize, search and enjoy your archived Youtube videos without hassle offline through a convenient web interface.
Once your YouTube video collection grows, it becomes hard to search and find a specific video. That's where Tube Archivist comes in: By indexing your video collection with metadata from YouTube, you can organize, search and enjoy your archived YouTube videos without hassle offline through a convenient web interface.
## Installation
Take a look at the example `docker-compose.yml` file provided. Tube Archivist depends on three main components split up into seperate docker containers:
## Connect
- [Discord](https://www.tubearchivist.com/discord): Connect with us on our Discord server.
- [r/TubeArchivist](https://www.reddit.com/r/TubeArchivist/): Join our Subreddit.
## Extended Universe
- [Browser Extension](https://github.com/tubearchivist/browser-extension) Tube Archivist Companion, for [Firefox](https://addons.mozilla.org/addon/tubearchivist-companion/) and [Chrome](https://chrome.google.com/webstore/detail/tubearchivist-companion/jjnkmicfnfojkkgobdfeieblocadmcie)
- [Tube Archivist Metrics](https://github.com/tubearchivist/tubearchivist-metrics) to create statistics in Prometheus/OpenMetrics format.
## Installing and updating
There's dedicated user-contributed install steps under [docs/Installation.md](./docs/Installation.md) for podman, Unraid, Truenas and Synology which you can use instead of this section if you happen to be using one of those. Otherwise, continue on.
For minimal system requirements, the Tube Archivist stack needs around 2GB of available memory for a small testing setup and around 4GB of available memory for a mid to large sized installation. Minimal with dual core with 4 threads, better quad core plus.
This project requires docker. Ensure it is installed and running on your system.
Note for **arm64**: Tube Archivist is a multi arch container, same for redis. For Elasitc Search use the official image for arm64 support. Other architectures are not supported.
Save the [docker-compose.yml](./docker-compose.yml) file from this reposity somewhere permanent on your system, keeping it named `docker-compose.yml`. You'll need to refer to it whenever starting this application.
Edit the following values from that file:
- under `tubearchivist`->`environment`:
- `HOST_UID`: your UID, if you want TubeArchivist to create files with your UID. Remove if you are OK with files being owned by the the container user.
- `HOST_GID`: as above but GID.
- `TA_HOST`: change it to the address of the machine you're running this on. This can be an IP address or a domain name.
- `TA_PASSWORD`: pick a password to use when logging in.
- `ELASTIC_PASSWORD`: pick a password for the elastic service. You won't need to type this yourself.
- `TZ`: your time zone. If you don't know yours, you can look it up [here](https://www.timezoneconverter.com/cgi-bin/findzone/findzone).
- under `archivist-es`->`environment`:
- `"ELASTIC_PASSWORD=verysecret"`: change `verysecret` to match the ELASTIC_PASSWORD you picked above.
By default Docker will store all data, including downloaded data, in its own data-root directory (which you can find by running `docker info` and looking for the "Docker Root Dir"). If you want to use other locations, you can replace the `media:`, `cache:`, `redis:`, and `es:` volume names with absolute paths; if you do, remove them from the `volumes:` list at the bottom of the file.
From a terminal, `cd` into the directory you saved the `docker-compose.yml` file in and run `docker compose up --detach`. The first time you do this it will download the appropriate images, which can take a minute.
You can follow the logs with `docker compose logs -f`. Once it's ready it will print something like `celery@1234567890ab ready`. At this point you should be able to go to `http://your-host:8000` and log in with the `TA_USER`/`TA_PASSWORD` credentials.
You can bring the application down by running `docker compose down` in the same directory.
Use the *latest* (the default) or a named semantic version tag for the docker images. The *unstable* tag is for intermediate testing and as the name implies, is **unstable** and not be used on your main installation but in a [testing environment](CONTRIBUTING.md).
## Installation Details
Tube Archivist depends on three main components split up into separate docker containers:
### Tube Archivist
The main Python application that displays and serves your video collection, built with Django.
- Serves the interface on port `8000`
- Needs a mandatory volume for the video archive at **/youtube**
- And another recommended volume to save the cache for thumbnails and artwork at **/cache**.
- Needs a volume for the video archive at **/youtube**
- And another volume to save application data at **/cache**.
- The environment variables `ES_URL` and `REDIS_HOST` are needed to tell Tube Archivist where Elasticsearch and Redis respectively are located.
- The environment variables `HOST_UID` and `HOST_GID` allowes Tube Archivist to `chown` the video files to the main host system user instead of the container user.
- The environment variables `HOST_UID` and `HOST_GID` allows Tube Archivist to `chown` the video files to the main host system user instead of the container user. Those two variables are optional, not setting them will disable that functionality. That might be needed if the underlying filesystem doesn't support `chown` like *NFS*.
- Set the environment variable `TA_HOST` to match with the system running Tube Archivist. This can be a domain like *example.com*, a subdomain like *ta.example.com* or an IP address like *192.168.1.20*, add without the protocol and without the port. You can add multiple hostnames separated with a space. Any wrong configurations here will result in a `Bad Request (400)` response.
- Change the environment variables `TA_USERNAME` and `TA_PASSWORD` to create the initial credentials.
- `ELASTIC_PASSWORD` is for the password for Elasticsearch. The environment variable `ELASTIC_USER` is optional, should you want to change the username from the default *elastic*.
- For the scheduler to know what time it is, set your timezone with the `TZ` environment variable, defaults to *UTC*.
- Set the environment variable `ENABLE_CAST=True` to send videos to your cast device, [read more](#enable-cast).
### Port collisions
If you have a collision on port `8000`, best solution is to use dockers *HOST_PORT* and *CONTAINER_PORT* distinction: To for example change the interface to port 9000 use `9000:8000` in your docker-compose file.
Should that not be an option, the Tube Archivist container takes these two additional environment variables:
- **TA_PORT**: To actually change the port where nginx listens, make sure to also change the ports value in your docker-compose file.
- **TA_UWSGI_PORT**: To change the default uwsgi port 8080 used for container internal networking between uwsgi serving the django application and nginx.
Changing any of these two environment variables will change the files *nginx.conf* and *uwsgi.ini* at startup using `sed` in your container.
### LDAP Authentication
You can configure LDAP with the following environment variables:
- `TA_LDAP` (ex: `true`) Set to anything besides empty string to use LDAP authentication **instead** of local user authentication.
- `TA_LDAP_SERVER_URI` (ex: `ldap://ldap-server:389`) Set to the uri of your LDAP server.
- `TA_LDAP_DISABLE_CERT_CHECK` (ex: `true`) Set to anything besides empty string to disable certificate checking when connecting over LDAPS.
- `TA_LDAP_BIND_DN` (ex: `uid=search-user,ou=users,dc=your-server`) DN of the user that is able to perform searches on your LDAP account.
- `TA_LDAP_BIND_PASSWORD` (ex: `yoursecretpassword`) Password for the search user.
- `TA_LDAP_USER_ATTR_MAP_USERNAME` (default: `uid`) Bind attribute used to map LDAP user's username
- `TA_LDAP_USER_ATTR_MAP_PERSONALNAME` (default: `givenName`) Bind attribute used to match LDAP user's First Name/Personal Name.
- `TA_LDAP_USER_ATTR_MAP_SURNAME` (default: `sn`) Bind attribute used to match LDAP user's Last Name/Surname.
- `TA_LDAP_USER_ATTR_MAP_EMAIL` (default: `mail`) Bind attribute used to match LDAP user's EMail address
- `TA_LDAP_USER_BASE` (ex: `ou=users,dc=your-server`) Search base for user filter.
- `TA_LDAP_USER_FILTER` (ex: `(objectClass=user)`) Filter for valid users. Login usernames are matched using the attribute specified in `TA_LDAP_USER_ATTR_MAP_USERNAME` and should not be specified in this filter.
When LDAP authentication is enabled, django passwords (e.g. the password defined in TA_PASSWORD), will not allow you to login, only the LDAP server is used.
### Enable Cast
As Cast doesn't support authentication, enabling this functionality will make your static files like artwork and media files accessible by guessing the links. That's read only access, the application itself is still protected.
Enabling this integration will embed an additional third party JS library from **Google**.
**Requirements**:
- HTTPS: To use the cast integration HTTPS needs to be enabled, which can be done using a reverse proxy. This is a requirement by Google as communication to the cast device is required to be encrypted, but the content itself is not.
- Supported Browser: A supported browser is required for this integration such as Google Chrome. Other browsers, especially Chromium-based browsers, may support casting by enabling it in the settings.
- Subtitles: Subtitles are supported however they do not work out of the box and require additional configuration. Due to requirements by Google, to use subtitles you need additional headers which will need to be configured in your reverse proxy. See this [page](https://developers.google.com/cast/docs/web_sender/advanced#cors_requirements) for the specific requirements.
You need the following headers: Content-Type, Accept-Encoding, and Range. Note that the last two headers, Accept-Encoding and Range, are additional headers that you may not have needed previously.
Wildcards "*" can not be used for the Access-Control-Allow-Origin header. If the page has protected media content, it must use a domain instead of a wildcard.
### Elasticsearch
**Note**: Tube Archivist depends on Elasticsearch 8.
Use `bbilly1/tubearchivist-es` to automatically get the recommended version, or use the official image with the version tag in the docker-compose file.
Use official Elastic Search for **arm64**.
Stores video meta data and makes everything searchable. Also keeps track of the download queue.
- Needs to be accessable over the default port `9200`
- Needs to be accessible over the default port `9200`
- Needs a volume at **/usr/share/elasticsearch/data** to store data
Follow the [documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/docker.html) for additional installation details.
### Elasticsearch on a custom port
Should you need to change the port for Elasticsearch to for example *9500*, follow these steps:
- Set the environment variable `http.port=9500` to the ES container
- Change the *expose* value for the ES container to match your port number
- For the Tube Archivist container, change the *ES_URL* environment variable, e.g. `ES_URL=http://archivist-es:9500`
### Redis JSON
Functions as a cache and temporary link between the application and the filesystem. Used to store and display messages and configuration variables.
- Needs to be accessable over the default port `6379`
- Takes an optional volume at **/data** to make your configuration changes permanent.
Functions as a cache and temporary link between the application and the file system. Used to store and display messages and configuration variables.
- Needs to be accessible over the default port `6379`
- Needs a volume at **/data** to make your configuration changes permanent.
## Getting Started
1. Go through the **settings** page and look at the available options. Particularly set *Download Format* to your desired video quality before downloading. **Tube Archivist** downloads the best available quality by default.
2. Subscribe to some of your favourite Youtube channels on the **channels** page.
3. On the **downloads** page, click on *Rescan subscriptions* to add videos from the subscribed channels to your Download queue or click on *Add to download queue* to manually add Video IDs, links, channels or playlists.
4. Click on *Download queue* and let Tube Archivist to it's thing.
5. Enjoy your archived collection!
## Import your existing library
So far this depends on the video you are trying to import to be still available on youtube to get the metadata. Add the files you like to import to the */cache/import* folder. Then start the process from the settings page *Manual media files import*. Make sure to follow one of the two methods below.
### Redis on a custom port
For some architectures it might be required to run Redis JSON on a nonstandard port. To for example change the Redis port to **6380**, set the following values:
- Set the environment variable `REDIS_PORT=6380` to the *tubearchivist* service.
- For the *archivist-redis* service, change the ports to `6380:6380`
- Additionally set the following value to the *archivist-redis* service: `command: --port 6380 --loadmodule /usr/lib/redis/modules/rejson.so`
### Method 1:
Add a matching *.json* file with the media file. Both files need to have the same base name, for example:
- For the media file: \<base-name>.mp4
- For the JSON file: \<base-name>.info.json
- Alternate JSON file: \<base-name>.json
### Updating Tube Archivist
You will see the current version number of **Tube Archivist** in the footer of the interface. There is a daily version check task querying tubearchivist.com, notifying you of any new releases in the footer. To take advantage of the latest fixes and improvements, make sure you are running the *latest and greatest*.
* There can be breaking changes between updates, particularly as the application grows, new environment variables or settings might be required for you to set in the your docker-compose file. *Always* check the **release notes**: Any breaking changes will be marked there.
* All testing and development is done with the Elasticsearch version number as mentioned in the provided *docker-compose.yml* file. This will be updated when a new release of Elasticsearch is available. Running an older version of Elasticsearch is most likely not going to result in any issues, but it's still recommended to run the same version as mentioned. Use `bbilly1/tubearchivist-es` to automatically get the recommended version.
**Tube Archivist** then looks for the 'id' key within the JSON file to identify the video.
### Helm charts
### Method 2:
Detect the Youtube ID from filename, this accepts the default yt-dlp naming convention for file names like:
- \<base-name>[\<youtube-id>].mp4
- The Youtube ID in square brackets at the end of the filename is the crucial part.
There is a Helm Chart available at https://github.com/insuusvenerati/helm-charts. Mostly self-explanatory but feel free to ask questions in the discord / subreddit.
### Some notes:
- This will **consume** the files you put into the import folder: Files will get converted to mp4 if needed (this might take a long time...) and moved to the archive, *.json* files will get deleted upon completion to avoid having doublicates on the next run.
- Maybe start with a subset of your files to import to make sure everything goes well...
- Follow the logs to monitor progress and errors: `docker-compose logs -f tubearchivist`.
## Potential pitfalls
**Elastic Search** in Docker requires the kernel setting of the host machine `vm.max_map_count` to be set to least 262144.
## Common Errors
### vm.max_map_count
**Elastic Search** in Docker requires the kernel setting of the host machine `vm.max_map_count` to be set to at least 262144.
To temporary set the value run:
```
@@ -94,28 +201,100 @@ To apply the change permanently depends on your host operating system:
- On Arch based systems create a file */etc/sysctl.d/max_map_count.conf* with the content `vm.max_map_count = 262144`.
- On any other platform look up in the documentation on how to pass kernel parameters.
### Permissions for elasticsearch
If you see a message similar to `Unable to access 'path.repo' (/usr/share/elasticsearch/data/snapshot)` or `failed to obtain node locks, tried [/usr/share/elasticsearch/data]` and `maybe these locations are not writable` when initially starting elasticsearch, that probably means the container is not allowed to write files to the volume.
To fix that issue, shutdown the container and on your host machine run:
```
chown 1000:0 -R /path/to/mount/point
```
This will match the permissions with the **UID** and **GID** of elasticsearch process within the container and should fix the issue.
### Disk usage
The Elasticsearch index will turn to *read only* if the disk usage of the container goes above 95% until the usage drops below 90% again, you will see error messages like `disk usage exceeded flood-stage watermark`, [link](https://github.com/tubearchivist/tubearchivist#disk-usage).
Similar to that, TubeArchivist will become all sorts of messed up when running out of disk space. There are some error messages in the logs when that happens, but it's best to make sure to have enough disk space before starting to download.
## Getting Started
1. Go through the **settings** page and look at the available options. Particularly set *Download Format* to your desired video quality before downloading. **Tube Archivist** downloads the best available quality by default. To support iOS or MacOS and some other browsers a compatible format must be specified. For example:
```
bestvideo[vcodec*=avc1]+bestaudio[acodec*=mp4a]/mp4
```
2. Subscribe to some of your favorite YouTube channels on the **channels** page.
3. On the **downloads** page, click on *Rescan subscriptions* to add videos from the subscribed channels to your Download queue or click on *Add to download queue* to manually add Video IDs, links, channels or playlists.
4. Click on *Start download* and let **Tube Archivist** to it's thing.
5. Enjoy your archived collection!
## Roadmap
This should be considered as a **minimal viable product**, there is an exstensive list of future functions and improvements planned.
We have come far, nonetheless we are not short of ideas on how to improve and extend this project. Issues waiting for you to be tackled in no particular order:
### Functionality
- [ ] Access controll
- [ ] User roles
- [ ] Delete videos and channel
- [ ] Create playlists
- [ ] Backup and restore
- [ ] Podcast mode to serve channel as mp3
- [ ] Implement [PyFilesystem](https://github.com/PyFilesystem/pyfilesystem2) for flexible video storage
- [X] Scan your filesystem to index already downloaded videos [2021-09-14]
### UI
- [ ] Show similar videos on video page
- [ ] Implement [Apprise](https://github.com/caronc/apprise) for notifications ([#97](https://github.com/tubearchivist/tubearchivist/issues/97))
- [ ] User created playlists, random and repeat controls ([#108](https://github.com/tubearchivist/tubearchivist/issues/108), [#220](https://github.com/tubearchivist/tubearchivist/issues/220))
- [ ] Auto play or play next link ([#226](https://github.com/tubearchivist/tubearchivist/issues/226))
- [ ] Multi language support
- [ ] Grid and list view for both channel and video list pages
- [ ] Show total video downloaded vs total videos available in channel
- [ ] Add statistics of index
- [ ] Download speed schedule ([#198](https://github.com/tubearchivist/tubearchivist/issues/198))
- [ ] Download or Ignore videos by keyword ([#163](https://github.com/tubearchivist/tubearchivist/issues/163))
- [ ] Custom searchable notes to videos, channels, playlists ([#144](https://github.com/tubearchivist/tubearchivist/issues/144))
Implemented:
- [X] Download video comments [2022-11-30]
- [X] Show similar videos on video page [2022-11-30]
- [X] Implement complete offline media file import from json file [2022-08-20]
- [X] Filter and query in search form, search by url query [2022-07-23]
- [X] Make items in grid row configurable to use more of the screen [2022-06-04]
- [X] Add passing browser cookies to yt-dlp [2022-05-08]
- [X] Add [SponsorBlock](https://sponsor.ajay.app/) integration [2022-04-16]
- [X] Implement per channel settings [2022-03-26]
- [X] Subtitle download & indexing [2022-02-13]
- [X] Fancy advanced unified search interface [2022-01-08]
- [X] Auto rescan and auto download on a schedule [2021-12-17]
- [X] Optional automatic deletion of watched items after a specified time [2021-12-17]
- [X] Create playlists [2021-11-27]
- [X] Access control [2021-11-01]
- [X] Delete videos and channel [2021-10-16]
- [X] Add thumbnail embed option [2021-10-16]
- [X] Create a github wiki for user documentation [2021-10-03]
- [X] Grid and list view for both channel and video list pages [2021-10-03]
- [X] Un-ignore videos [2021-10-03]
- [X] Dynamic download queue [2021-09-26]
- [X] Backup and restore [2021-09-22]
- [X] Scan your file system to index already downloaded videos [2021-09-14]
## Known limitations
- Video files created by Tube Archivist need to be **mp4** video files for best browser compatibility.
- Video files created by Tube Archivist need to be playable in your browser of choice. Not every codec is compatible with every browser and might require some testing with format selection.
- Every limitation of **yt-dlp** will also be present in Tube Archivist. If **yt-dlp** can't download or extract a video for any reason, Tube Archivist won't be able to either.
- For now this is meant to be run in a trusted network environment.
- There is currently no flexibility in naming of the media files.
## Donate
The best donation to **Tube Archivist** is your time, take a look at the [contribution page](CONTRIBUTING.md) to get started.
Second best way to support the development is to provide for caffeinated beverages:
* [GitHub Sponsor](https://github.com/sponsors/bbilly1) become a sponsor here on GitHub
* [Paypal.me](https://paypal.me/bbilly1) for a one time coffee
* [Paypal Subscription](https://www.paypal.com/webapps/billing/plans/subscribe?plan_id=P-03770005GR991451KMFGVPMQ) for a monthly coffee
* [ko-fi.com](https://ko-fi.com/bbilly1) for an alternative platform
## Notable mentions
This is a selection of places where this project has been featured on reddit, in the news, blogs or any other online media. Sorted by newest on the top.
* **console.substack.com**: Interview With Simon of Tube Archivist, [2023-01-29] [[link](https://console.substack.com/p/console-142#%C2%A7interview-with-simon-of-tube-archivist)]
* **reddit.com**: Tube Archivist v0.3.0 - Now Archiving Comments, [2022-12-02] [[link](https://www.reddit.com/r/selfhosted/comments/zaonzp/tube_archivist_v030_now_archiving_comments/)]
* **reddit.com**: Tube Archivist v0.2 - Now with Full Text Search, [2022-07-24] [[link](https://www.reddit.com/r/selfhosted/comments/w6jfa1/tube_archivist_v02_now_with_full_text_search/)]
* **noted.lol**: How I Control What Media My Kids Watch Using Tube Archivist, [2022-03-27] [[link](https://noted.lol/how-i-control-what-media-my-kids-watch-using-tube-archivist/)]
* **thehomelab.wiki**: Tube Archivist - A Youtube-DL Alternative on Steroids, [2022-01-27] [[link](https://thehomelab.wiki/books/news/page/tube-archivist-a-youtube-dl-alternative-on-steroids)]
* **reddit.com**: Celebrating TubeArchivist v0.1, [2022-01-09] [[link](https://www.reddit.com/r/selfhosted/comments/rzh084/celebrating_tubearchivist_v01/)]
* **linuxunplugged.com**: Pick: tubearchivist — Your self-hosted YouTube media server, [2021-09-11] [[link](https://linuxunplugged.com/425)] and [2021-10-05] [[link](https://linuxunplugged.com/426)]
* **reddit.com**: Introducing Tube Archivist, your self hosted Youtube media server, [2021-09-12] [[link](https://www.reddit.com/r/selfhosted/comments/pmj07b/introducing_tube_archivist_your_self_hosted/)]
## Sponsor
Big thank you to [Digitalocean](https://www.digitalocean.com/) for generously donating credit for the tubearchivist.com VPS and buildserver.
<p>
<a href="https://www.digitalocean.com/">
<img src="https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/PoweredByDO/DO_Powered_by_Badge_blue.svg" width="201px">
</a>
</p>

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 138 KiB

After

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 156 KiB

After

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

After

Width:  |  Height:  |  Size: 238 KiB

188
deploy.sh
View File

@@ -3,17 +3,22 @@
# deploy all needed project files to different servers:
# test for local vm for testing
# blackhole for local production
# docker to publish
# unstable to publish intermediate releases
# docker to publish regular release
# create builder:
# docker buildx create --name tubearchivist
# docker buildx use tubearchivist
# docker buildx inspect --bootstrap
# more details:
# https://github.com/tubearchivist/tubearchivist/issues/6
set -e
function sync_blackhole {
# docker commands need sudo
host="blackhole.local"
read -sp 'Password: ' remote_pw
export PASS=$remote_pw
rsync -a --progress --delete-after \
--exclude ".git" \
@@ -23,16 +28,21 @@ function sync_blackhole {
--exclude "db.sqlite3" \
. -e ssh "$host":tubearchivist
echo "$PASS" | ssh "$host" 'sudo -S docker build -t bbilly1/tubearchivist:latest tubearchivist 2>/dev/null'
echo "$PASS" | ssh "$host" 'sudo -S docker-compose up -d 2>/dev/null'
ssh "$host" 'docker build -t bbilly1/tubearchivist --build-arg TARGETPLATFORM="linux/amd64" tubearchivist'
ssh "$host" 'docker compose up -d'
}
function sync_test {
# docker commands don't need sudo in testing vm
host="tubearchivist.local"
# pass argument to build for specific platform
host="tubearchivist.local"
# make base folder
ssh "$host" "mkdir -p docker"
# copy project files to build image
rsync -a --progress --delete-after \
--exclude ".git" \
--exclude ".gitignore" \
@@ -41,41 +51,179 @@ function sync_test {
--exclude "db.sqlite3" \
. -e ssh "$host":tubearchivist
rsync -r --progress --delete docker-compose.yml -e ssh "$host":docker
# copy default docker-compose file if not exist
rsync --progress --ignore-existing docker-compose.yml -e ssh "$host":docker
ssh "$host" 'docker build -t bbilly1/tubearchivist:latest tubearchivist'
ssh "$host" 'docker-compose -f docker/docker-compose.yml up -d'
if [[ $1 = "amd64" ]]; then
platform="linux/amd64"
elif [[ $1 = "arm64" ]]; then
platform="linux/arm64"
elif [[ $1 = "multi" ]]; then
platform="linux/amd64,linux/arm64"
else
platform="linux/amd64"
fi
ssh "$host" 'docker cp tubearchivist/tubearchivist/testing.sh tubearchivist:/app/testing.sh'
ssh "$host" 'docker exec tubearchivist chmod +x /app/testing.sh'
ssh "$host" "docker buildx build --build-arg INSTALL_DEBUG=1 --platform $platform -t bbilly1/tubearchivist:latest tubearchivist --load"
ssh "$host" 'docker compose -f docker/docker-compose.yml up -d'
}
function sync_docker {
# run same tests and checks as with github action but locally
# takes filename to validate as optional argument
function validate {
if [[ $1 ]]; then
check_path="$1"
else
check_path="."
fi
echo "run validate on $check_path"
# note: this logic is duplicated in the `./github/workflows/lint_python.yml` config
# if you update this file, you should update that as well
echo "running black"
black --exclude "migrations/*" --diff --color --check -l 79 "$check_path"
echo "running codespell"
codespell --skip="./.git,./package.json,./package-lock.json,./node_modules" "$check_path"
echo "running flake8"
flake8 "$check_path" --exclude "migrations" --count --max-complexity=10 \
--max-line-length=79 --show-source --statistics
echo "running isort"
isort --skip "migrations" --check-only --diff --profile black -l 79 "$check_path"
printf " \n> all validations passed\n"
}
# update latest tag compatible es for set and forget
function sync_latest_es {
VERSION=$(grep "bbilly1/tubearchivist-es" docker-compose.yml | awk '{print $NF}')
printf "\nsync new ES version %s\nContinue?\n" "$VERSION"
read -rn 1
if [[ $(systemctl is-active docker) != 'active' ]]; then
echo "starting docker"
sudo systemctl start docker
fi
sudo docker build -t bbilly1/tubearchivist:latest .
sudo docker push bbilly1/tubearchivist:latest
sudo docker image pull docker.elastic.co/elasticsearch/elasticsearch:"$VERSION"
sudo docker tag \
docker.elastic.co/elasticsearch/elasticsearch:"$VERSION" \
bbilly1/tubearchivist-es
sudo docker tag \
docker.elastic.co/elasticsearch/elasticsearch:"$VERSION" \
bbilly1/tubearchivist-es:"$VERSION"
sudo docker push bbilly1/tubearchivist-es
sudo docker push bbilly1/tubearchivist-es:"$VERSION"
}
# check package versions in requirements.txt for updates
python version_check.py
# publish unstable tag to docker
function sync_unstable {
if [[ $(systemctl is-active docker) != 'active' ]]; then
echo "starting docker"
sudo systemctl start docker
fi
# start amd64 build
sudo docker buildx build \
--platform linux/amd64 \
-t bbilly1/tubearchivist:unstable --push .
}
# new function, sync only tag, build with build server
function sync_docker {
# check things
if [[ $(git branch --show-current) != 'master' ]]; then
echo 'you are not on master, dummy!'
return
fi
echo "latest tags:"
git tag | tail -n 5 | sort -r
printf "\ncreate new version:\n"
read -r VERSION
echo "push new tag: $VERSION?"
read -rn 1
# create release tag
echo "commits since last version:"
git log "$(git describe --tags --abbrev=0)"..HEAD --oneline
git tag -a "$VERSION" -m "new release version $VERSION"
git push origin "$VERSION"
}
# old builder, sync tag, build and push locally
function sync_docker_old {
# check things
if [[ $(git branch --show-current) != 'master' ]]; then
echo 'you are not on master, dummy!'
return
fi
if [[ $(systemctl is-active docker) != 'active' ]]; then
echo "starting docker"
sudo systemctl start docker
fi
echo "latest tags:"
git tag | tail -n 5 | sort -r
printf "\ncreate new version:\n"
read -r VERSION
echo "build and push $VERSION?"
read -rn 1
# start build
sudo docker buildx build \
--platform linux/amd64,linux/arm64 \
-t bbilly1/tubearchivist \
-t bbilly1/tubearchivist:unstable \
-t bbilly1/tubearchivist:"$VERSION" --push .
# create release tag
echo "commits since last version:"
git log "$(git describe --tags --abbrev=0)"..HEAD --oneline
git tag -a "$VERSION" -m "new release version $VERSION"
git push origin "$VERSION"
}
if [[ $1 == "blackhole" ]]; then
sync_blackhole
elif [[ $1 == "test" ]]; then
sync_test
sync_test "$2"
elif [[ $1 == "validate" ]]; then
validate "$2"
elif [[ $1 == "versioncheck" ]]; then
python version_check.py
elif [[ $1 == "docker" ]]; then
sync_docker
elif [[ $1 == "unstable" ]]; then
sync_unstable
elif [[ $1 == "es" ]]; then
sync_latest_es
else
echo "valid options are: blackhole | test | docker"
echo "valid options are: blackhole | test | validate | versioncheck | docker | unstable | es"
fi

View File

@@ -3,43 +3,57 @@ version: '3.3'
services:
tubearchivist:
container_name: tubearchivist
restart: always
image: bbilly1/tubearchivist:latest
restart: unless-stopped
image: bbilly1/tubearchivist
ports:
- 8000:8000
volumes:
- ./volumes/tubearchivist/media:/youtube
- ./volumes/tubearchivist/cache:/cache
- media:/youtube
- cache:/cache
environment:
- ES_URL=http://archivist-es:9200
- REDIS_HOST=archivist-redis
- ES_URL=http://archivist-es:9200 # needs protocol e.g. http and port
- REDIS_HOST=archivist-redis # don't add protocol
- HOST_UID=1000
- HOST_GID=1000
- TA_HOST=tubearchivist.local # set your host name
- TA_USERNAME=tubearchivist # your initial TA credentials
- TA_PASSWORD=verysecret # your initial TA credentials
- ELASTIC_PASSWORD=verysecret # set password for Elasticsearch
- TZ=America/New_York # set your time zone
depends_on:
- archivist-es
- archivist-redis
archivist-redis:
image: redislabs/rejson:latest
image: redis/redis-stack-server
container_name: archivist-redis
restart: always
ports:
- 6379:6379
restart: unless-stopped
expose:
- "6379"
volumes:
- ./volumes/tubearchivist/redis:/data
- redis:/data
depends_on:
- archivist-es
archivist-es:
image: docker.elastic.co/elasticsearch/elasticsearch:7.14.1
image: bbilly1/tubearchivist-es # only for amd64, or use official es 8.6.0
container_name: archivist-es
restart: always
restart: unless-stopped
environment:
- "discovery.type=single-node"
- "ELASTIC_PASSWORD=verysecret" # matching Elasticsearch password
- "ES_JAVA_OPTS=-Xms512m -Xmx512m"
- "xpack.security.enabled=true"
- "discovery.type=single-node"
- "path.repo=/usr/share/elasticsearch/data/snapshot"
ulimits:
memlock:
soft: -1
hard: -1
volumes:
- ./volumes/tubearchivist/es:/usr/share/elasticsearch/data
ports:
- 9200:9200
- es:/usr/share/elasticsearch/data # check for permission error when using bind mount, see readme
expose:
- "9200"
volumes:
media:
cache:
redis:
es:

33
docker_assets/nginx.conf Normal file
View File

@@ -0,0 +1,33 @@
server {
listen 8000;
location /cache/videos/ {
auth_request /api/ping/;
alias /cache/videos/;
}
location /cache/channels/ {
auth_request /api/ping/;
alias /cache/channels/;
}
location /cache/playlists/ {
auth_request /api/ping/;
alias /cache/playlists/;
}
location /media/ {
auth_request /api/ping/;
alias /youtube/;
types {
text/vtt vtt;
}
}
location / {
include uwsgi_params;
uwsgi_pass localhost:8080;
}
}

23
docker_assets/run.sh Normal file
View File

@@ -0,0 +1,23 @@
#!/bin/bash
# startup script inside the container for tubearchivist
set -e
# django setup
python manage.py migrate
if [[ -z "$DJANGO_DEBUG" ]]; then
python manage.py collectstatic --noinput -c
fi
# ta setup
python manage.py ta_envcheck
python manage.py ta_connection
python manage.py ta_startup
# start all tasks
nginx &
celery -A home.tasks worker --loglevel=INFO &
celery -A home beat --loglevel=INFO \
-s "${BEAT_SCHEDULE_PATH:-${cachedir}/celerybeat-schedule}" &
uwsgi --ini uwsgi.ini

11
docker_assets/uwsgi.ini Normal file
View File

@@ -0,0 +1,11 @@
[uwsgi]
module = config.wsgi:application
master = True
pidfile = /tmp/project-master.pid
vacuum = True
max-requests = 5000
socket = :8080
buffer-size = 8192
log-5xx = true
log-4xx = true
disable-logging = true

49
docs/Channels.md Normal file
View File

@@ -0,0 +1,49 @@
# Channels Overview and Channel Detail Page
The channels are organized on two different levels, similar as the [playlists](Playlists):
## Channels Overview
Accessible at `/channel/` of your Tube Archivist, the **Overview Page** shows a list of all channels you have indexed.
- You can filter that list to show or hide subscribed channels with the toggle. Clicking on the channel banner or the channel name will direct you to the *Channel Detail Page*.
- If you are subscribed to a channel a *Unsubscribe* button will show, if you aren't subscribed, a *Subscribe* button will show instead.
The **Subscribe to Channels** button <img src="assets/icon-add.png?raw=true" alt="add icon" width="20px" style="margin:0 5px;"> opens a text field to subscribe to a channel. You have a few options:
- Enter the YouTube channel ID, a 25 character alphanumeric string. For example *UCBa659QWEk1AI4Tg--mrJ2A*
- Enter the URL to the channel page on YouTube. For example *https://www.youtube.com/channel/UCBa659QWEk1AI4Tg--mrJ2A* or alias url *https://www.youtube.com/@TomScottGo*
- Enter a channel alias starting with *@*, for example: *@TomScottGo*
- Enter the video URL for any video and let Tube Archivist extract the channel ID for you, for example *https://www.youtube.com/watch?v=2tdiKTSdE9Y*
- Add one per line.
To search your channels, click on the search icon <img src="assets/icon-search.png?raw=true" alt="search icon" width="20px" style="margin:0 5px;"> to reach the search page. Start your query with `channel:`, learn more on the [search](Search) page.
## Channel Detail
Each channel gets a set of channel detail pages.
- If you are subscribed to the channel, an *Unsubscribe* button will show, else the *Subscribe* button will show.
- The **Mark as Watched** button will mark all videos of this channel as watched.
### Videos
Accessible at `/channel/<channel-id>/`, this page shows all the videos you have downloaded from this channel.
### Streams
If you have any streams indexed, this page will become accessible at `/channel/<channel-id>/streams/`, this page shows all available live streams of that channel.
### Shorts
If you have any shorts videos indexed, this page will become accessible at `/channel/<channel-id>/shorts/`, this page shows all the shorts videos of that channel.
### Playlists
If you have playlists from this channel indexed, this page will become accessible at `/channel/<channel-id>/playlist/`. Activate channel playlist indexing on the about page.
### About
On the *Channel About* page, accessible at `/channel/<channel-id>/about/`, you can see additional metadata.
- The button **Delete Channel** will delete the channel plus all videos of this channel, both media files and metadata additionally this will also delete playlists metadata belonging to that channel.
- The button **Reindex** will reindex all channel metadata. This will also categorize existing videos as shorts or streams.
- The button **Reindex Videos** will reindex metadata for all videos in this channel.
The channel customize form gives options to change settings on a per channel basis. Any configurations here will overwrite your configurations from the [settings](Settings) page.
- **Download Format**: Overwrite the download quality for videos from this channel.
- **Auto Delete**: Automatically delete watched videos from this channel after selected days.
- **Index Playlists**: Automatically add all Playlists with at least a video downloaded to your index. Only do this for channels where you care about playlists as this will slow down indexing new videos for having to check which playlist this belongs to.
- **SponsorBlock**: Using [SponsorBlock](https://sponsor.ajay.app/) to get and skip sponsored content. Customize per channel: You can *disable* or *enable* SponsorBlock for certain channels only to overwrite the behavior set on the [Settings](settings) page. Selecting *unset* will remove the overwrite and your setting will fall back to the default on the settings page.
### Downloads
If you have any videos from this channel pending in the download queue, a *Downloads* link will show, bringing you directly to the [downloads](Downloads) page, filtering the list by the selected channel.

59
docs/Downloads.md Normal file
View File

@@ -0,0 +1,59 @@
# Downloads Page
Accessible at `/downloads/` of your Tube Archivist, this page handles all the download functionality.
## Rescan Subscriptions
The **Rescan Subscriptions** icon <img src="assets/icon-rescan.png?raw=true" alt="rescan icon" width="20px" style="margin:0 5px;"> will start a background task to look for new videos from the channels and playlists you are subscribed to.
Tube Archivist will get available *videos*, *shorts* and *streams* from each channel, you can define the channel and playlist page size on the [settings page](Settings#subscriptions). With the default page size, expect this process to take around 2-3 seconds for each channel or playlist you are subscribed to. A status message will show the progress.
Then for every video found, **Tube Archivist** will skip the video if it has already been downloaded or if you added it to the *ignored* list before. All the other videos will get added to the download queue. Expect this to take around 2 seconds for each video as **Tube Archivist** needs to grab some additional metadata and artwork. New videos will get added at the bottom of the download queue.
## Download Queue
The **Start Download** icon <img src="assets/icon-download.png?raw=true" alt="download icon" width="20px" style="margin:0 5px;"> will start the download process starting from the top of the queue. Take a look at the relevant settings on the [Settings Page](Settings#downloads). Once the process started, a progress message will show with additional details and controls:
- The stop icon <img src="assets/icon-stop.png?raw=true" alt="stop icon" width="20px" style="margin:0 5px;"> will gracefully stop the download process, once the current video has been finished successfully.
- The cancel icon <img src="assets/icon-close-red.png?raw=true" alt="close icon" width="20px" style="margin:0 5px;"> is equivalent to killing the process and will stop the download immediately. Any leftover files will get deleted, the canceled video will still be available in the download queue.
After downloading, Tube Archivist tries to add new videos to already indexed playlists and if activated on the settings page, get comments for the new videos.
## Add to Download Queue
The **Add to Download Queue** icon <img src="assets/icon-add.png?raw=true" alt="add icon" width="20px" style="margin:0 5px;"> opens a text field to manually add videos to the download queue. Add one item per line. You have a few options:
### Videos
- Add a YouTube video ID, for example *2tdiKTSdE9Y*
- Add a link to a YouTube video, for example *https://www.youtube.com/watch?v=2tdiKTSdE9Y*
- Add a link to a YouTube video by providing the shortened URL, for example *https://youtu.be/2tdiKTSdE9Y*
- Add a link to a shorts video, for example *https://www.youtube.com/shorts/UOfe6e0k7cQ*
### Channels
- When adding a channel, Tube Archivist will ignore the channel page size as described above, this is meant for an initial download of the whole channel. You can still ignore selected videos from the queue before starting the download.
- Download a complete channel including shorts and streams by entering:
- Channel ID: *UCBa659QWEk1AI4Tg--mrJ2A*
- Channel URL: *https://www.youtube.com/channel/UCBa659QWEk1AI4Tg--mrJ2A*
- Channel *@* alias handler: For example *@TomScottGo*
- Channel alias URL: *https://www.youtube.com/@TomScottGo*
- Download videos, live streams or shorts only, by providing a partial channel URL:
- Videos only: *https://www.youtube.com/@IBRACORP/videos*
- Shorts only: *https://www.youtube.com/@IBRACORP/shorts*
- Streams only: *https://www.youtube.com/@IBRACORP/streams*
- Every other channel sub page will default to download all, for example *https://www.youtube.com/@IBRACORP/featured* will download videos and shorts and streams.
### Playlist
- Add a playlist ID or URL to add every available video in the list to the download queue, for example *https://www.youtube.com/playlist?list=PL96C35uN7xGLLeET0dOWaKHkAlPsrkcha* or *PL96C35uN7xGLLeET0dOWaKHkAlPsrkcha*.
- When adding a playlist to the queue, this playlist will automatically get [indexed](Playlists#playlist-detail).
- When you add a link to a video in a playlist, Tube Archivist assumes you want to download only the specific video and not the whole playlist, for example *https://www.youtube.com/watch?v=CINVwWHlzTY&list=PL96C35uN7xGLLeET0dOWaKHkAlPsrkcha* will only add one video *CINVwWHlzTY* to the queue.
## The Download Queue
Below the three buttons you find the download queue. New items will get added at the bottom of the queue, the next video to download once you click on **Start Download** will be the first in the list.
You can filter the download queue with the **filter** dropdown box, the filter will show once you have more than one channel in the download queue. Select the channel to filter by name, the number in parentheses indicates how many videos you have pending from this channel. Reset the filter by selecting *all* from the dropdown. This will generate links for the top 30 channels with pending videos.
Every video in the download queue has two buttons:
- **Ignore**: This will remove that video from the download queue and this video will not get added again, even when you **Rescan Subscriptions**.
- **Download now**: This will give priority to this video. If the download process is already running, the prioritized video will get downloaded as soon as the current video is finished. If there is no download process running, this will start downloading this single video and stop after that.
You can flip the view by activating **Show Only Ignored Videos**. This will show all videos you have previously *ignored*.
Every video in the ignored list has two buttons:
- **Forget**: This will delete the item from the ignored list.
- **Add to Queue**: This will add the ignored video back to the download queue.
You can delete your download queue from the [Settings](Settings#actions) page.

36
docs/FAQ.md Normal file
View File

@@ -0,0 +1,36 @@
# Frequently Asked Questions
## 1. Scope of this project
Tube Archivist is *Your self hosted YouTube media server*, which also defines the primary scope of what this project tries to do:
- **Self hosted**: This assumes you have full control over the underlying operating system and hardware and can configure things to work properly with Docker, it's volumes and networks as well as whatever disk storage and filesystem you choose to use.
- **YouTube**: Downloading, indexing and playing videos from YouTube, there are currently no plans to expand this to any additional platforms.
- **Media server**: This project tries to be a stand alone media server in it's own web interface.
Additionally to that, progress is also happening on:
- **API**: Endpoints for additional integrations.
- **Browser Extension**: To integrate between youtube.com and Tube Archivist.
Defining the scope is important for the success of any project:
- A scope too broad will result in development effort spreading too thin and will run into danger that his project tries to do too many things and none of them well.
- A too narrow scope will make this project uninteresting and will exclude audiences that could also benefit from this project.
- Not defining a scope will easily lead to misunderstandings and false hopes of where this project tries to go.
Of course this is subject to change: The scope can be expanded as this project continues to grow and more people contribute.
## 2. Emby/Plex/Jellyfin/Kodi integrations
Although there are similarities between these excellent projects and Tube Archivist, they have a very different use case. Trying to fit the metadata relations and database structure of a YouTube archival project into these media servers that specialize in Movies and TV shows is always going to be limiting.
Part of the scope is to be its own media server, so that's where the focus and effort of this project is. That being said, the nature of self hosted and open source software gives you all the possible freedom to use your media as you wish.
## 3. To Docker or not to Docker
This project is a classical docker application: There are multiple moving parts that need to be able to interact with each other and need to be compatible with multiple architectures and operating systems. Additionally Docker also drastically reduces development complexity which is highly appreciated.
So Docker is the only supported installation method. If you don't have any experience with Docker, consider investing the time to learn this very useful technology.
## 4. Finetuning Elasticsearch
A minimal configuration of Elasticsearch (ES) is provided in the example docker-compose.yml file. ES is highly configurable and very interesting to learn more about. Refer to the [documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/index.html) if you want to get into it.
## 5. When I subscribe to a channel it only downloads the most recent 50 videos
Subscribing to a channel is a different operation from downloading it. You can [add the channel to the download queue](https://github.com/tubearchivist/tubearchivist/wiki/Downloads#add-to-download-queue) to download all past videos.
If you want to download the existing videos and also automatically download new videos, then you should both download the channel and subscribe to it.

41
docs/Home.md Normal file
View File

@@ -0,0 +1,41 @@
# Tube Archivist Wiki
Welcome to the official Tube Archivist Wiki. This is an up-to-date documentation of user functionality.
Table of contents:
* [FAQ](FAQ): Frequently asked questions what this project is and tries to do
* [Channels](Channels): Browse your channels, handle channel subscriptions
* [Playlists](Playlists): Browse your indexed playlists, handle playlist subscriptions
* [Downloads](Downloads): Scanning subscriptions, handle download queue
* [Settings](Settings): All the configuration options
* [Video](Video): All details of a single video and playlist navigation.
* [Users](Users): User management admin interface
* [Search](Search): Search your archive
* [Installation](Installation): Detailed installation instructions for various platforms.
## Getting Started
1. [Subscribe](Channels#channels-overview) to some of your favourite YouTube channels.
2. [Scan](Downloads#rescan-subscriptions) subscriptions to add the latest videos to the download queue.
3. [Add](Downloads#add-to-download-queue) additional videos, channels or playlist - ignore the ones you don't want to download.
4. [Download](Downloads#download-queue) and let **Tube Archivist** do it's thing.
5. Sit back and enjoy your archived and indexed collection!
## General Navigation
* Clicking on the channel name or the channel icon brings you to the dedicated channel page to show videos from that channel.
* Clicking on a video title brings you to the dedicated video page and shows additional details.
* Clicking on a video thumbnail opens the video player and starts streaming the selected video.
* Clicking on the search icon <img src="assets/icon-search.png?raw=true" alt="gridview icon" width="20px" style="margin:0 5px;"> will open a dedicated search page to search over your complete index.
* The pagination - if available - builds links for up to 10'000 results, use the search, sort or filter functionality to find what you are looking for.
An empty checkbox icon <img src="assets/icon-unseen.png?raw=true" alt="unseen icon" width="20px" style="margin:0 5px;"> will show for videos you haven't marked as watched. Click on it and the icon will change to a filled checkbox <img src="assets/icon-seen.png?raw=true" alt="seen icon" width="20px" style="margin:0 5px;"> indicating it as watched - click again to revert.
When available the <img src="assets/icon-gridview.png?raw=true" alt="gridview icon" width="20px" style="margin:0 5px;"> gridview icon will display the list in a grid. A grid row holds 3 items by default, use the <img src="assets/icon-add.png?raw=true" alt="listview icon" width="20px" style="margin:0 5px;"> icon to add more or the <img src="assets/icon-substract.png?raw=true" alt="listview icon" width="20px" style="margin:0 5px;"> icon to remove items per row, depending on your screen size. The <img src="assets/icon-listview.png?raw=true" alt="listview icon" width="20px" style="margin:0 5px;"> listview icon will arrange the items in a list. The sort icon <img src="assets/icon-sort.png?raw=true" alt="listview icon" width="20px" style="margin:0 5px;"> will open additional sort options.
You can control the video player with the following keyboard shortcuts:
- `?`: Show help
- `m`: toggle mute
- `c`: toggle subtitles if available
- `>`: increase playback speed
- `<`: decrease playback speed
- `←` (left arrow): jump back 5 seconds
- `→` (right arrow): jump forward 5 seconds

288
docs/Installation.md Normal file
View File

@@ -0,0 +1,288 @@
# Detailed Installation Instructions for Various Platforms
## Table of Contents
- [Podman](#podman)
- [Unraid](#unraid)
- [Truenas Scale](#truenas-scale)
- [Synology](#synology)
These are beginners guides installation instructions for additional platforms generously provided by users of these platforms. When in doubt, verify the details with the [Readme](https://github.com/tubearchivist/tubearchivist#installing-and-updating). If you see any issues here while using these instructions, please contribute.
## Podman
Podman handles container hostname resolving slightly differently than docker, so you need to make a few changes to the `docker-compose.yml` to get up and running.
### Step 1: Follow the installation instructions from the [README](https://github.com/tubearchivist/tubearchivist#installing-and-updating), with a few additional changes to the `docker-compose.yml`.
Edit these additional changes to the `docker-compose.yml`:
- under `tubearchivist`->`image`:
- prefix the container name with `docker.io/` (or the url of your repo of choice).
- under `tubearchivist`->`environment`:
- `ES_URL`: change `archivist-es` to the internal IP of the computer that will be running the containers.
- `REDIS_HOST`: change `archivist-redis` to the internal IP of the computer that will be running the containers (should be the same as above).
- under `archivist-redis`->`image`:
- prefix the container name with `docker.io/` again.
- under `archivist-redis`->`expose`:
- change the whole entry from `expose: ["<PORT>"]` into `ports: ["<PORT>:<PORT>"].
- under `archivist-es`->`image`:
- prefix the container name with `docker.io/` again.
- under `archivist-es`->`expose`:
- change the whole entry from `expose: ["<PORT>"]` into `ports: ["<PORT>:<PORT>"].
### Step 2: Create service files (optional)
Since podman doesn't run as a service, it can't start containers after reboots, at least not without some help.
If you want to enable this behavior, you can follow [this example](https://techblog.jeppson.org/2020/04/create-podman-services-with-podman-compose/) to have `systemd` start up the containers with `podman-compose` when the computer boots up.
## Unraid
Tube Archivist, and all if it's dependencies are located in the [community applications](https://unraid.net/community/apps?q=tubearchivist) store. The three containers you will need are as follows:
- **TubeArchivist-RedisJSON**: This container acts as a cache and temporary link between the application and the file system. Used to store and display messages and configuration variables.
- **TubeArchivist-ES**: ElasticSearch stores video metadata and makes everything searchable. Also keeps track of the download queue.
- **TubeArchivist**: Once your YouTube video collection grows, it becomes hard to search and find a specific video. That's where Tube Archivist comes in: By indexing your video collection with metadata from YouTube, you can organize, search and enjoy your archived YouTube videos without hassle offline through a convenient web interface.
### Step 1: Install `TubeArchivist-RedisJSON`
![TubeArchivist-RedisJSON](https://i.imgur.com/iGJZKYd.png)
This is the easiest container to setup of the thee, just make sure that you do not have any port conflicts, and that your `/data` is mounted to the correct path. The other containers will map to the same root directory (/mnt/user/appdata/TubeArchivist).
If you need to install `TubeArchivist-RedisJSON`on a different port, you'll have to follow [these steps](https://github.com/tubearchivist/tubearchivist#redis-on-a-custom-port) later on when installing the `TubeArchivist` container
### Step 2: Install `TubeArchivist-ES`
![TubeArchivist-ES](https://i.imgur.com/RC38b3u.png)
ElasticSeach is also pretty easy to setup. Again, make sure you have no port conflicts, make sure that you mapped the ElasticSearch Data to the same root directory as `RedisJSON` (/mnt/user/appdata/TubeArchivist), and make sure to change the default password to something more secure.
There is four additional settings in the "show more settings" area, but don't require any changes.
### Step 3: Install `TubeArchivist`
![TubeArchivist](https://i.imgur.com/oLsmwBZ.png)
It's finally time to set up TubeArchivist!
- `HOST:`This is a list of IP addresses that you will host TA from. Example, 192.168.0.14 is the IP address of my Unraid server. If I was going to access TA from a VPN, or domain name, I'd put those next to my host IP with just a space separating the different addresses. More information [here.](https://github.com/tubearchivist/tubearchivist#installing-and-updating)
- `Port:`Again, make sure that you have no port conflicts on 8000.
- `Youtube Media Path:` is where you'll download all of your videos to.
Make sure that this is an empty directory to not cause confusion when
starting the application. If you have existing videos that you'd like
to import into Tube Archivist, please checkout the [settings
wiki.](https://github.com/tubearchivist/tubearchivist/wiki/Settings#manual-media-files-import)
- `Appdata:` This should be the same base path as the other two containers (/mnt/user/appdata/TubeArchivist).
- `TA Username:`This will be your username for TubeArchivist.
- `TA Password:`This will be your password for TubeArchivist.
- `Redis` This will be JUST the ip address of your redis container.
- `ElasticSearch Password:`This is the password you defined in the `TubeArchivist-ES` container.
- `ElasticSearch:` This seems to cause some confusion, but it's a pretty simple step, just replace the IP and Port to match you `TubeArchivist-ES` container.
(example: if your IP is 192.168.0.14, the value should be http://192.168.0.14:9200)
- `Time Zone:` This is an important step for your scheduler, to find your timezone, use a site like [TimeZoneConverter](http://www.timezoneconverter.com/cgi-bin/findzone.tzc)
### From there, you should be able to start up your containers and you're good to go!
If you're still having trouble, join us on [discord](https://www.tubearchivist.com/discord) and come to the #support channel.
<br />
<br />
## Truenas Scale
Truenas Scale can be a bit confusing, with its k3s kubernetes implementation.
However, there is a step by step guide available for it's users here:
https://heavysetup.info/applications/tube-archivist/dataset/
- Ensure you are navigating the columns under `Tube Archivist` on the left hand side of the screen
<br />
<br />
## Synology
There are several different methods to install TubeArchivist on Synology platforms. This will focus on the available `docker` package and `docker-compose` implementations.
### Prepare Directories/Folders
Before we setup TubeArchivist, we need to setup the directories/folders. You are assumed to be logged into the Synology NAS.
#### 1. Docker Base Folder
1. Open the `File Station` utility.
2. Click on the **Create🔽** button and choose *Create New Shared Folder*.
3. **Name** the folder "Docker".
4. Add a **Description**.
5. Select the **Volume Location**.
> Note: By default, this will be where all data is stored. Change the folders as best meets your requirements.
6. Select the appropriate options from the remaining checkbox configurations.
![Synology - Create Docker Folder](assets/Synology_0.2.0_Docker-Folder-Create.png)
7. Click the **Next** button.
8. If you are going to **Encrypt** your folder, check the appropriate box and provide the Encryption Key and its confirmation.
9. Click the **Next** button.
10. On the **Advanced Settings** page, you can select the *Enable data checksum for advanced data integrity* setting. This may cause a performance impact, but will allow for potential file self-healing. **This cannot be changed later.**
> Note: This is not recommended, as we will be hosting databases within this folder.
11. If you are enabling a quota for how large the folder can get, you can select the *Enabled shared folder quota* setting and choose the maximum size this folder can grow. This can be changed later.
12. Click the **Next** button.
13. Confirm the settings, then click the **Apply** button. This will create the folder.
#### 2. TubeArchivist Base Folder
1. Open the `File Station` utility.
2. Select the "Docker" folder on the left-hand side.
3. Click on the `Create🔽` button and choose *create Folder*.
4. **Name** the folder "TubeArchivist".
#### 3. Redis Data
1. Open the `File Station` utility.
2. Select the "Docker" folder on the left-hand side.
3. Select the "TubeArchivist" folder beneath "Docker".
4. Click on the `Create🔽` button and choose *create Folder*.
5. **Name** the folder "redis".
#### 4. Elastic Search Data
1. Open the `File Station` utility.
2. Select the "Docker" folder on the left-hand side.
3. Select the "TubeArchivist" folder beneath "Docker".
4. Click on the `Create🔽` button and choose *create Folder*.
5. **Name** the folder "es".
#### 5. TubeArchivist Cache
1. Open the `File Station` utility.
2. Select the "Docker" folder on the left-hand side.
3. Select the "TubeArchivist" folder beneath "Docker".
4. Click on the `Create🔽` button and choose *create Folder*.
5. **Name** the folder "cache".
#### 6. TubeArchivist Output
1. Open the `File Station` utility.
2. Select the "Docker" folder on the left-hand side.
3. Select the "TubeArchivist" folder beneath "Docker".
4. Click on the `Create🔽` button and choose *create Folder*.
5. **Name** the folder "media".
#### 7. Confirm Folder Structure
Once all of the folders have been created, it should have a folder structure within Docker\TubeArchivist that includes "cache", "es", "media", and "redis" folders.
![Synology - Docker Folder Structure](assets/Synology_0.2.0_Docker-Folder-Structure.png)
#### 8. Change Permissions - CLI Required
> If you do not have SSH access enabled for CLI, [enable it](https://kb.synology.com/en-sg/DSM/tutorial/How_to_login_to_DSM_with_root_permission_via_SSH_Telnet) before continuing.
1. Open the SSH connection to the Synology. Login as your primary `Admin` user, or the user that was enabled for SSH access.
2. Elevate your access to `root`. Steps are provided [here](https://kb.synology.com/en-sg/DSM/tutorial/How_to_login_to_DSM_with_root_permission_via_SSH_Telnet).
3. Change directories to the **Volume** where the "Docker" folder resides.
</br>Example: `cd /volume1`
4. Change directories to the "Docker" folder.
</br>Example: `cd Docker`
5. Change directories to the "TubeArchivist" folder.
</br>Example: `cd TubeArchivist`
6. Change the owner of the "redis" folder. *If correct, this does not have an output.*
</br>Example: `chown 999:100 redis`
7. Change the owner of the "es" folder. *If correct, this does not have an output.*
</br>Example: `chown 1000:1000 es`
8. Confirm that the folders have the correct permissions.
</br>Example: `ls -hl`
![Synology - Docker Folder Permissions Command](assets/Synology_0.2.0_Docker-Folder-Permissions-Commands.png)
9. Logout from root.
</br>Example: `logout`
10. Disconnect from the SSH connection.
</br>Example: `exit`
### Docker Setup
1. Install the `Docker` Synology Package.
1. Log in to your Synology NAS.
2. Open the `Package Center` utility.
3. Search for `Docker`.
4. Click `Install`.
![Synology - Install Docker Utility](assets/Synology_0.2.0_Docker-Install.png)
2. After `Docker` is installed, open the `Docker` utility.
3. Go to the `Registry` tab.
4. Search for the following `images` and download them. Follow the recommended versions for each of the images.
- `redislabs/rejson`
![Synology - Redis Image Search](assets/Synology_0.2.0_Docker-Redis-Search.png)
- `bbilly1/tubearchivist-es`
![Synology - ElasticSearch Image Search](assets/Synology_0.2.0_Docker-ES-Search.png)
- `bbilly1/tubearchivist`
![Synology - TubeArchivist Image Search](assets/Synology_0.2.0_Docker-TA-Search.png)
5. Go to the `Image` tab. From here, create an container based on each image with the associated configurations below.
- ElasticSearch
1. Select the associated image.
2. Click the **Launch** button in the top.
3. Edit the **Container Name** to be "tubearchivist-es".
4. Click on the **Advanced Settings** button.
5. In the **Advanced Settings** tab, check the box for `Enable auto-restart`.
6. In the **Volume** tab, click the **Add Folder** button and select the "Docker/TubeArchivist/es" folder, then type in `/usr/share/elasticsearch/data` for the mount path.
7. In the **Network** tab, leave the default `bridge` Network (unless you have a specific Network design that you know how to implement).
8. In the **Port Settings** tab, replace the "Auto" entry under **Local Port** with the port that will be used to connect to ElasticSearch (default is 9200).
9. In the **Port Settings** tab, select the entryline for port 9300 and ** delete** the line. It is not needed for this container.
10. The **Links** tab does not require configuration for this container.
11. In the **Environment** tab, add in the following ElasticSearch specific environment variables that may apply.
- "discovery.type=single-node"
- "ES_JAVA_OPTS=-Xms512m -Xmx512m"
- "UID=1000"
- "GID=0"
- "xpack.security.enabled=true"
- "ELASTIC_PASSWORD=verysecret"
- "path.repo=/usr/share/elasticsearch/data/snapshot"
> Do not use the default password as it is very insecure.
> Activating snapshots for backups should only be done *after* setting the `path.repo` setting.
![Synology - ElasticSearch Environment Configurations](assets/Synology_0.2.0_Docker-ES-Env-Conf.png)
12. Click on the **Apply** button.
13. Back on the **Create Container** screen, click the **Next** button.
14. Review the settings to confirm, then click the **Apply** button.
- Redis
1. Select the associated image.
2. Click the **Launch** button in the top.
3. Edit the **Container Name** to be "tubearchivist-redis".
4. Click on the **Advanced Settings** button.
5. In the **Advanced Settings** tab, check the box for `Enable auto-restart`.
6. In the **Volume** tab, click the **Add Folder** button and select the "Docker/TubeArchivist/redis" folder, then type in `/data` for the mount path.
7. In the **Network** tab, leave the default `bridge` Network (unless you have a specific Network design that you know how to implement).
8. In the **Port Settings** tab, replace the "Auto" entry under **Local Port** with the port that will be used to connect to Redis (default is 6379).
9. In the **Links** tab, select the "tubearchivist-es" container from the **Container Name** dropdown and provide it the same alias, "tubearchivist-es".
10. In the **Environment** tab, add in any Redis specific environment variables that may apply (none by default).
11. Click on the **Apply** button.
12. Back on the **Create Container** screen, click the **Next** button.
13. Review the settings to confirm, then click the **Apply** button.
- TubeArchivist
1. Select the associated image.
2. Click the **Launch** button in the top.
3. Edit the **Container Name** to be "tubearchivist".
4. Click on the **Advanced Settings** button.
5. In the **Advanced Settings** tab, check the box for `Enable auto-restart`.
6. In the **Volume** tab, click the **Add Folder** button and select the "Docker/TubeArchivist/cache" folder, then type in `/cache` for the mount path.
7. In the **Volume** tab, click the **Add Folder** button and select the "Docker/TubeArchivist/media" folder, then type in `/youtube` for the mount path.
8. In the **Network** tab, leave the default `bridge` Network (unless you have a specific Network design that you know how to implement).
9. In the **Port Settings** tab, replace the "Auto" entry under **Local Port** with the port that will be used to connect to TubeArchivist (default is 8000).
10. In the **Links** tab, select the "tubearchivist-es" container from the **Container Name** dropdown and provide it the same alias, "tubearchivist-es".
11. In the **Links** tab, select the "tubearchivist-redis" container from the **Container Name** dropdown and provide it the same alias, "tubearchivist-redis".
12. In the **Environment** tab, add in the following TubeArchivist specific environment variables that may apply. **Change the variables as-is appropriate to your use case. Follow the [README section](https://github.com/tubearchivist/tubearchivist#tube-archivist) for details on what to set each variable.**
- "TA_HOST=synology.local"
- "ES_URL=http://tubearchivist-es:9200"
- "REDIS_HOST=tubearchivist-redis"
- "HOST_UID=1000"
- "HOST_GID=1000"
- "TA_USERNAME=tubearchivist"
- "TA_PASSWORD=verysecret"
- "ELASTIC_PASSWORD=verysecret"
- "TZ=America/New_York"
> Do not use the default password as it is very insecure.
> Ensure that ELASTIC_PASSWORD matches the password used on the tubearchivist-es container.
![Synology - TubeArchivist Environment Configurations](assets/Synology_0.2.0_Docker-TA-Env-Conf.png)
13. Click on the **Apply** button.
14. Back on the **Create Container** screen, click the **Next** button.
15. Review the settings to confirm, then click the **Apply** button.
6. After the containers have been configured and started, you can go to the **Container** tab and monitor the containers.
7. To review the logs to ensure that the system has started successfully, select the "tubearchivist" container and click on the **Details** button. In the new window, go to the **Log** tab. Monitor the logs until either an error occurs or the message `celery@tubearchivist ready.` is in the logs. This may take a few minutes, especially for a first time setup.
> Note: Synology Docker presents the logs in a pagination format. If you are not seeing the logs update, check if there are additional pages.
8. After it has started, go to the location in the `TA_HOST`. This should give you the standard TubeArchivist login screen.
<!--
### Docker-Compose Setup -->
<!-- This section is a Work In Progress -->
### From there, you should be able to start up your containers and you're good to go!
If you're still having trouble, join us on [discord](https://www.tubearchivist.com/discord) and come to the #support channel.

25
docs/Playlists.md Normal file
View File

@@ -0,0 +1,25 @@
# Playlist Overview and Playlist Detail Page
The playlists are organized in two different levels, similar as the [channels](Channels):
## Playlist Overview
Accessible at `/playlist/` of your Tube Archivist, this **Overview Page** shows a list of all playlists you have indexed over all your channels.
- You can filter that list to show only subscribed to playlists with the toggle.
You can index playlists of a channel from the channel detail page as described [here](Channels#channel-detail).
The **Subscribe to Playlist** button <img src="assets/icon-add.png?raw=true" alt="add icon" width="20px" style="margin:0 5px;"> opens a text field to subscribe to playlists. You have a few options:
- Enter the YouTube playlist id, for example: *PL96C35uN7xGLLeET0dOWaKHkAlPsrkcha*
- Enter the Youtube dedicated playlist url, for example: *https://www.youtube.com/playlist?list=PL96C35uN7xGLLeET0dOWaKHkAlPsrkcha*
- Add one per line.
- NOTE: It doesn't make sense to subscribe to a playlist if you are already subscribed the corresponding channel as this will slow down the **Rescan Subscriptions** [task](Downloads#rescan-subscriptions).
You can search your indexed playlists by clicking on the search icon <img src="assets/icon-search.png?raw=true" alt="search icon" width="20px" style="margin:0 5px;">. This will open a dedicated page.
## Playlist Detail
Each playlist will get a dedicated playlist detail page accessible at `/playlist/<playlist-id>/` of your Tube Archivist. This page shows all the videos you have downloaded from this playlist.
- If you are subscribed to the playlist, an Unsubscribe button will show, else the Subscribe button will show.
- The **Mark as Watched** button will mark all videos of this playlist as watched.
- The button **Reindex** will reindex the playlist metadata.
- The button **Reindex Videos** will reindex all videos from this playlist.
- The **Delete Playlist** button will give you the option to delete just the *metadata* which won't delete any media files or *delete all* which will delete metadata plus all videos belonging to this playlist.

57
docs/Search.md Normal file
View File

@@ -0,0 +1,57 @@
# Search Page
Accessible at `/search/` of your **Tube Archivist**, search your archive for Videos, Channels and Playlists - or even full text search throughout your indexed subtitles.
- All your queries are case insensitive and are normalized to lowercase.
- All your queries are analyzed for the english language, this means *singular*, *plural* and word variations like *-ing*, *-ed*, *-able* etc are treated as synonyms.
- Fuzzy search is activated for all your searches by default. This can catch typos in your queries or in the matching documents with one to two letters difference, depending on the query length. You can configure fuzziness with the secondary keyword `fuzzy:`, e.g:
- `fuzzy:0` or `fuzzy:no`: Deactivate fuzzy matching.
- `fuzzy:1`: Set fuzziness to one letter difference.
- `fuzzy:2`: Set fuzziness to two letters difference.
- All text searches are ranked, meaning the better a match the higher ranked the result. Unless otherwise stated, queries with multiple words are processed with the `and` operator, meaning all words need to match so each word will narrow down the result.
- This will return 30 results per query, pagination is not implemented yet.
Just start typing to start a *simple* search or start your query with a primary keyword to search for a specific type and narrow down the result with secondary keywords. Secondary keywords can be in any order. Use *yes* or *no* for boolean values.
## Simple
Start your query without a keyword to make a simple query. This will search in *video titles*, *channel names* and *playlist titles* and will return matching videos, channels and playlists. Keyword searches will return more results in a particular category due to the fact that more fields are searched for matches.
## Video
Start your query with the primary keyword `video:` to search for videos only. This will search through the *video titles*, *tags* and *category* fields. Narrow your search down with secondary keywords:
- `channel:` search for videos matching the channel name.
- `active:` is a boolean value, to search for videos that are still active on youtube or that are not active any more.
**Example**:
- `video:learn python channel:corey shafer active:yes`: This will return all videos with the term *Learn Python* from the channel *Corey Shafer* that are still *Active* on YouTube.
- `video: channel:tom scott active:no`: Note the omitted term after the primary key, this will show all videos from the channel *Tom Scott* that are no longer active on YouTube.
## Channel
Start with the `channel:` primary keyword to search for channels matching your query. This will search through the *channel name* and *channel description* fields. Narrow your search down with secondary keywords:
- `subscribed:` is a boolean value, search for channels that you are subscribed to or not.
- `active:` is a boolean value, to search for channels that are still active on YouTube or that are no longer active.
**Example**:
- `channel:linux subscribed:yes`: Search for channels with the term *Linux* that you are subscribed to.
- `channel: active:no`: Note the omitted term after the primary key, this will return all channels that are no longer active on YouTube.
## Playlist
Start your query with the primary keyword `playlist:` to search for playlists only. This will search through the *playlist title* and *playlist description* fields. Narrow down your search with these secondary keywords:
- `subscribed`: is a boolean value, search for playlists that you are subscribed to or not.
- `active:` is a boolean value, to search for playlists that are still active on YouTube or that are no longer active.
**Example**:
- `playlist:backend engineering subscribed:yes`: Search for playlists about *Backend Engineering* that you are subscribed to.
- `playlist: active:yes subscribed:yes`: Note the omitted primary search term, this will return all playlists active on YouTube that you are subscribed to.
- `playlist:html css active:yes`: Search for playlists containing *HTML CSS* that are still active on YouTube.
## Full
Start a full text search by beginning your query with the primary keyword `full:`. This will search through your indexed Subtitles showing segments with possible matches. This will only show any results if you have activated *subtitle download and index* on the settings page. The operator for full text searches is `or` meaning when searching for multiple words not all words need to match, but additional words will change the ranking of the result, the more words match and the better they match, the higher ranked the result. The matching words will get highlighted in the text preview.
Clicking the play button on the thumbnail will open the inplace player at the timestamp from where the segment starts. Same when clicking the video title, this will open the video page and put the player at the segment timestamp. This will overwrite any previous playback position.
Narrow down your search with these secondary keywords:
- `lang`: Search for matches only within a language. Use the same two letter ISO country code as you have set on the settings page.
- `source`: Can either be *auto* to search through auto generated subtitles only or *user* to search through user uploaded subtitles only.
**Example**:
- `full:contribute to open source lang:en` search for subtitle segments matching with the words *Contribute to Open Source* in the language *en*.
- `full:flight simulator cockpit source:user` to search for the words *Flight Simulator Cockpit* from *user* uploaded subtitle segments.

190
docs/Settings.md Normal file
View File

@@ -0,0 +1,190 @@
# Settings Page
Accessible at `/settings/` of your **Tube Archivist**, this page holds all the configurations and additional functionality related to the database.
Click on **Update Settings** at the bottom of the form to apply your configurations.
## Color scheme
Switch between the easy on the eyes dark theme and the burning bright theme.
## Archive View
- **Page Size**: Defines how many results get displayed on a given page. Same value goes for all archive views.
## Subscriptions
Settings related to the channel management. Disable shorts or streams by setting their page size to 0 (zero).
- **Channel Page Size**: Defines how many pages will get analyzed by **Tube Archivist** each time you click on *Rescan Subscriptions*. The default page size used by yt-dlp is **50**, that's also the recommended value to set here. Any value higher will slow down the rescan process, for example if you set the value to 51, that means yt-dlp will have to go through 2 pages of results instead of 1 and by that doubling the time that process takes.
- **Live Page Size**: Same as above, but for channel live streams.
- **Shorts page Size**: Same as above, but for shorts videos.
## Downloads
Settings related to the download process.
- **Download Limit**: Stop the download process after downloading the set quantity of videos.
- **Download Speed Limit**: Set your download speed limit in KB/s. This will pass the option `--limit-rate` to yt-dlp.
- **Throttled Rate Limit**: Restart download if the download speed drops below this value in KB/s. This will pass the option `--throttled-rate` to yt-dlp. Using this option might have a negative effect if you have an unstable or slow internet connection.
- **Sleep Interval**: Time in seconds to sleep between requests to YouTube. It's a good idea to set this to **3** seconds. Might be necessary to avoid throttling.
- **Auto Delete Watched Videos**: Automatically delete videos marked as watched after selected days. If activated, checks your videos after download task is finished.
## Download Format
Additional settings passed to yt-dlp.
- **Format**: This controls which streams get downloaded and is equivalent to passing `--format` to yt-dlp. Use one of the recommended one or look at the documentation of [yt-dlp](https://github.com/yt-dlp/yt-dlp#format-selection). Please note: The option `--merge-output-format mp4` is automatically passed to yt-dlp to guarantee browser compatibility. Similar to that, `--check-formats` is passed as well to check that the selected formats are actually downloadable.
- **Embed Metadata**: This saves the available tags directly into the media file by passing `--embed-metadata` to yt-dlp.
- **Embed Thumbnail**: This will save the thumbnail into the media file by passing `--embed-thumbnail` to yt-dlp.
## Subtitles
- **Download Setting**: Select the subtitle language you like to download. Add a comma separated list for multiple languages.
- **Source Settings**: User created subtitles are provided from the uploader and are usually the video script. Auto generated is from YouTube, quality varies, particularly for auto translated tracks.
- **Index Settings**: Enabling subtitle indexing will add the lines to Elasticsearch and will make subtitles searchable. This will increase the index size and is not recommended on low-end hardware.
## Comments
- **Download and index comments**: Set your configuration for downloading and indexing comments. This takes the same values as documented in the `max_comments` section for the youtube extractor of [yt-dlp](https://github.com/yt-dlp/yt-dlp#youtube). Add without space between the four different fields: *max-comments,max-parents,max-replies,max-replies-per-thread*. Example:
- `all,100,all,30`: Get 100 max-parents and 30 max-replies-per-thread.
- `1000,all,all,50`: Get a total of 1000 comments over all, 50 replies per thread.
- **Comment sort method**: Change sort method between *top* or *new*. The default is *top*, as decided by YouTube.
- The [Refresh Metadata](#refresh-metadata) background task will get comments from your already archived videos, spreading the requests out over time.
Archiving comments is slow as only very few comments get returned per request with yt-dlp. Choose your configuration above wisely. Tube Archivist will download comments after the download queue finishes, your videos will be already available while the comments are getting downloaded.
## Cookie
Importing your YouTube Cookie into Tube Archivist allows yt-dlp to bypass age restrictions, gives access to private videos and your *watch later* or *liked videos*.
### Security concerns
Cookies are used to store your session and contain your access token to your google account, this information can be used to take over your account. Treat that data with utmost care as you would any other password or credential. *Tube Archivist* stores your cookie in Redis and will automatically append it to yt-dlp for every request.
### Auto import
Easiest way to import your cookie is to use the **Tube Archivist Companion** [browser extension](https://github.com/tubearchivist/browser-extension) for Firefox and Chrome.
### Alternative Manual Export your cookie
- Install **Cookies.txt** addon for [chrome](https://chrome.google.com/webstore/detail/get-cookiestxt/bgaddhkoddajcdgocldbbfleckgcbcid) or [firefox](https://addons.mozilla.org/firefox/addon/cookies-txt).
- Visit YouTube and login with whichever YouTube account you wish to use to generate the cookies.
- Click on the extension icon in the toolbar - it will drop down showing the active cookies for YT.
- Click Export to export the cookies, filename is by default *cookies.google.txt*.
### Alternative Manual Import your cookie
Place the file *cookies.google.txt* into the *cache/import* folder of Tube Archivist and enable the cookie import. Once you click on *Update Application Configurations* to save your changes, your cookie will get imported and stored internally.
Once imported, a **Validate Cookie File** button will show, where you can confirm if your cookie is working or not.
### Use your cookie
Once imported, additionally to the advantages above, your [Watch Later](https://www.youtube.com/playlist?list=WL) and [Liked Videos](https://www.youtube.com/playlist?list=LL) become a regular playlist you can download and subscribe to as any other [playlist](Playlists).
### Limitation
There is only one cookie per Tube Archivist instance, this will be shared between all users.
## Integrations
All third party integrations of TubeArchivist will **always** be *opt in*.
- **API**: Your access token for the Tube Archivist API.
- **returnyoutubedislike.com**: This will get return dislikes and average ratings for each video by integrating with the API from [returnyoutubedislike.com](https://www.returnyoutubedislike.com/).
- **SponsorBlock**: Using [SponsorBlock](https://sponsor.ajay.app/) to get and skip sponsored content. If a video doesn't have timestamps, or has unlocked timestamps, use the browser addon to contribute to this excellent project. Can also be activated and deactivated as a per [channel overwrite](Settings#channel-customize).
## Snapshots
System snapshots will automatically make daily snapshots of the Elasticsearch index. The task will start at 12pm your local time. Snapshots are deduplicated, meaning that each snapshot will only have to backup changes since the last snapshot. The initial snapshot may be slow, but subsequent runs will be much faster. There is also a cleanup function implemented, that will remove snapshots older than 30 days.
This will make a snapshot of your metadata index only, no media files or additional configuration variables you have set on the settings page will be backed up.
Due to these improvements compared to the previous backup solution, system snapshots will replace the current backup system in a future version.
Before activating system snapshots, you'll have to add one additional environment variables to the *archivist-es* container:
```
path.repo=/usr/share/elasticsearch/data/snapshot
```
The variable `path.repo` will set the folder where the snapshots will go inside the Elasticsearch container, you can't change it, but the variable needs to be set. Rebuild the container for changes to take effect, e.g `docker compose up -d`.
- **Create snapshot now**: Will start the snapshot process now, outside of the regular daily schedule.
- **Restore**: Restore your index to that point in time.
# Scheduler Setup
Schedule settings expect a cron like format, where the first value is minute, second is hour and third is day of the week. Day 0 is Sunday, day 1 is Monday etc.
Examples:
- **0 15 \***: Run task every day at 15:00 in the afternoon.
- **30 8 \*/2**: Run task every second day of the week (Sun, Tue, Thu, Sat) at 08:30 in the morning.
- **0 \*/3,8-17 \***: Execute every hour divisible by 3, and every hour during office hours (8 in the morning - 5 in the afternoon).
- **0 8,16 \***: Execute every day at 8 in the morning and at 4 in the afternoon.
- **auto**: Sensible default.
- **0**: (zero), deactivate that task.
NOTE:
- Changes in the scheduler settings require a container restart to take effect.
- Cron format as *number*/*number* are none standard cron and are not supported by the scheduler, for example **0 0/12 \*** is invalid, use **0 \*/12 \*** instead.
- Avoid an unnecessary frequent schedule to not get blocked by YouTube. For that reason, the scheduler doesn't support schedules that trigger more than once per hour.
## Rescan Subscriptions
That's the equivalent task as run from the downloads page looking through your channel and playlist and add missing videos to the download queue.
## Start download
Start downloading all videos currently in the download queue.
## Refresh Metadata
Rescan videos, channels and playlists on youtube and update metadata periodically. This will also refresh your subtitles based on your current settings. If an item is no longer available on YouTube, this will deactivate it and exclude it from future refreshes. This task is meant to be run once per day, set your schedule accordingly.
The field **Refresh older than x days** takes a number where TubeArchivist will consider an item as *outdated*. This value is used to calculate how many items need to be refreshed today based on the total indexed. This will spread out the requests to YouTube. Sensible value here is **90** days.
## Thumbnail check
This will check if all expected thumbnails are there and will delete any artwork without matching video.
## ZIP file index backup
Create a zip file of the metadata and select **Max auto backups to keep** to automatically delete old backups created from this task. For data consistency, make sure there aren't any other tasks running that will change the index during the backup process. This is very slow, particularly for large archives. Use snapshots instead.
# Actions
Additional database functionality.
## Delete download queue
The button **Delete all queued** will delete all pending videos from the download queue. The button **Delete all ignored** will delete all videos you have previously ignored.
## Manual Media Files Import
NOTE: This is inherently error prone, as there are many variables, some outside of the control of this project. Read this carefully and use at your own risk.
Add the files you'd like to import to the */cache/import* folder. Only add files, don't add subdirectories. All files you are adding, need to have the same *base name* as the media file. Then start the process from the settings page *Manual Media Files Import*.
Valid media extensions are *.mp4*, *.mkv* or *.webm*. If you have other file extensions or incompatible codecs, convert them first to mp4. **Tube Archivist** can identify the videos with one of the following methods.
### Method 1:
Add a matching *.info.json* file with the media file. Both files need to have the same base name, for example:
- For the media file: `<base-name>.mp4`
- For the JSON file: `<base-name>.info.json`
The import process then looks for the 'id' key within the JSON file to identify the video.
### Method 2:
Detect the YouTube ID from filename, this accepts the default yt-dlp naming convention for file names like:
- `<base-name>[<youtube-id>].mp4`
- The YouTube ID in square brackets at the end of the filename is the crucial part.
### Offline import:
If the video you are trying to import is not available on YouTube any more, **Tube Archivist** can import the required metadata:
- The file `<base-name>.info.json` is required to extract the required information.
- Add the thumbnail as `<base-name>.<ext>`, where valid file extensions are *.jpg*, *.png* or *.webp*. If there is no thumbnail file, **Tube Archivist** will try to extract the embedded cover from the media file or will fallback to a default thumbnail.
- Add subtitles as `<base-name>.<lang>.vtt` where *lang* is the two letter ISO country code. This will archive all subtitle files you add to the import folder, independent from your configurations. Subtitles can be archived and used in the player, but they can't be indexed or made searchable due to the fact, that they have a very different structure than the subtitles as **Tube Archivist** needs them.
- For videos, where the whole channel is not available any more, you can add the `<channel-id>.info.json` file as generated by *youtube-dl/yt-dlp* to get the full metadata. Alternatively **Tube Archivist** will extract as much info as possible from the video info.json file.
### Some notes:
- This will **consume** the files you put into the import folder: Files will get converted to mp4 if needed (this might take a long time...) and moved to the archive, *.json* files will get deleted upon completion to avoid having duplicates on the next run.
- For best file transcoding quality, convert your media files with desired settings first before importing.
- Maybe start with a subset of your files to import to make sure everything goes well...
- Follow the logs to monitor progress and errors: `docker-compose logs -f tubearchivist`.
## Embed thumbnails into media file
This will write or overwrite all thumbnails in the media file using the downloaded thumbnail. This is only necessary if you didn't download the files with the option *Embed Thumbnail* enabled or want to make sure all media files get the newest thumbnail. Follow the docker-compose logs to monitor progress.
## ZIP file index backup
This will backup your metadata into a zip file. The file will get stored at *cache/backup* and will contain the necessary files to restore the Elasticsearch index formatted **nd-json** files. For data consistency, make sure there aren't any other tasks running that will change the index during the backup process. This is very slow, particularly for large archives.
BE AWARE: This will **not** backup any media files, just the metadata from the Elasticsearch.
## Restore From Backup
The restore functionality will expect the same zip file in *cache/backup* as created from the **Backup database** function. This will recreate the index from the snapshot. There will be a list of all available backup to choose from. The *source* tag can have these different values:
- **manual**: For backups manually created from here on the settings page.
- **auto**: For backups automatically created via a sceduled task.
- **update**: For backups created after a Tube Archivist update due to changes in the index.
- **False**: Undefined.
BE AWARE: This will **replace** your current index with the one from the backup file. This won't restore any media files.
## Rescan Filesystem
This function will go through all your media files and looks at the whole index to try to find any issues:
- Should the filename not match with the indexed media url, this will rename the video files correctly and update the index with the new link.
- When you delete media files from the filesystem outside of the Tube Archivist interface, this will delete leftover metadata from the index.
- When you have media files that are not indexed yet, this will grab the metadata from YouTube like it was a newly downloaded video. This can be useful when restoring from an older backup file with missing metadata but already downloaded mediafiles. NOTE: This only works if the media files are named in the same convention as Tube Archivist does, particularly the YouTube ID needs to be at the same index in the filename, alternatively see above for *Manual Media Files Import*.
-This will also check all of your thumbnails and download any that are missing.
BE AWARE: There is no undo.

20
docs/Users.md Normal file
View File

@@ -0,0 +1,20 @@
# User Management
For now, **Tube Archivist** is a single user application. You can create multiple users with different names and passwords, they will share the same videos and permissions but some interface configurations are on a per user basis. *More is on the roadmap*.
## Superuser
The first user gets created with the environment variables **TA_USERNAME** and **TA_PASSWORD** from your docker-compose file. That first user will automatically have *superuser* privileges.
## Admin Interface
When logged in from your *superuser* account, you are able to access the admin interface from the settings page or at `/admin/`. This interface holds all functionality for user management.
## Create additional users
From the admin interface when you click on *Accounts* you will get a list of all users. From there you can create additional users by clicking on *Add Account*, provide a name and confirm password and click on *Save* to create the user.
## Changing users
You can delete or change permissions and password of a user by clicking on the username from the *Accounts* list page and follow the interface from there. Changing the password of the *superuser* here will overwrite the password originally set with the environment variables.
## Reset
Delete all user configurations by deleting the file `cache/db.sqlite3` and restart the container. This will create the superuser again from the environment variables.
NOTE: Future improvements here will most likely require such a reset.

25
docs/Video.md Normal file
View File

@@ -0,0 +1,25 @@
# Video Page
Every video downloaded gets a dedicated page accessible at `/video/<video-id>/` of your Tube Archivist. Throughout the interface, click on a video title to access the video page.
Clicking on the channel name or the channel icon will bring you to the dedicated channel detail [page](Channels#channel-detail).
- The button **Reindex** will reindex the metadata of this video.
- The button **Download File** will download the media file in the browser.
- The button **Delete Video** will delete that video including the media file.
If available, a tag cloud will show, representing the tags set by the uploader.
The video description is truncated to the first few lines, click on *show more* to expand the whole description.
## Playlist
When available, a playlist navigation will show at the bottom. Clicking on the playlist name will bring you to the dedicated [Playlist Detail](Playlists#playlist-detail) page showing all videos downloaded from that playlist. The number in square brackets indicates the position of the current video in that playlist.
Clicking on the next or previous video name or thumbnail will bring you to that dedicated video page.
## Similar Videos
Tube Archivist will show up to six similar videos in a grid. Similarity is detected from the **video title** and the **video tags**. This naturally will show some videos from the same channel, but can also return videos about the same topic from other channels.
When playing a video from the similar section with the inline player, the current video will get replaced, refresh the page to reset that or click on the video title to avoid that behavior.
## Comments
If activated on the settings page, this will show the indexed comments. Reveal the threads by clicking the *+ Replies* button. Comments with a heart symbol are favorited by the uploader, comments by the uploader are highlighted in a different color.

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

BIN
docs/assets/icon-add.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

BIN
docs/assets/icon-rescan.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

BIN
docs/assets/icon-search.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

BIN
docs/assets/icon-seen.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

BIN
docs/assets/icon-sort.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

BIN
docs/assets/icon-stop.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

BIN
docs/assets/icon-unseen.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@@ -1,18 +0,0 @@
server {
listen 8000;
location /cache/ {
alias /cache/;
}
location /media/ {
alias /youtube/;
}
location / {
include uwsgi_params;
uwsgi_pass localhost:8080;
}
}

1955
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

17
package.json Normal file
View File

@@ -0,0 +1,17 @@
{
"private": true,
"scripts": {
"lint": "eslint 'tubearchivist/static/**/*.js'",
"format": "prettier --write 'tubearchivist/static/**/*.js'"
},
"devDependencies": {
"eslint": "^8.26.0",
"prettier": "^2.7.1",
"eslint-config-prettier": "^8.5.0"
},
"prettier": {
"singleQuote": true,
"arrowParens": "avoid",
"printWidth": 100
}
}

20
run.sh
View File

@@ -1,20 +0,0 @@
#!/bin/bash
# startup script inside the container for tubearchivist
counter=0
until curl "$ES_URL" -fs; do
echo "waiting for elastic search to start"
counter=$((counter+1))
if [[ $counter -eq 12 ]]; then
# fail after 1 min
echo "failed to connect to elastic search, exiting..."
exit 1
fi
sleep 5
done
python manage.py migrate
python manage.py collectstatic
nginx &
celery -A home.tasks worker --loglevel=INFO &
uwsgi --ini uwsgi.ini

441
tubearchivist/api/README.md Normal file
View File

@@ -0,0 +1,441 @@
# TubeArchivist API
Documentation of available API endpoints.
Note:
- This is very early stages and will change!
- Check the commit history to see if a documented feature is already in your release
## Table of contents
- [Authentication](#authentication)
- [Pagination](#pagination)
**Video**
- [Video List](#video-list-view)
- [Video Single](#video-item-view)
- [Video Comments](#video-comment-view)
- [Video Similar](#video-similar-view)
- [Video Single Progress](#video-progress-view)
- [Video Single Sponsorblock](#sponsor-block-view) WIP
**Channel**
- [Channel List](#channel-list-view)
- [Channel Single](#channel-item-view)
- [Channel Video List](#channel-videos-view)
**Playlist**
- [Playlist List](#playlist-list-view)
- [Playlist Single](#playlist-item-view)
- [Playlist Videos List](#playlist-videos-view)
**Download queue**
- [Download Queue List](#download-queue-list-view)
- [Download Queue Single](#download-queue-item-view)
**Snapshot management**
- [Snapshot List](#snapshot-list-view)
- [Snapshot Single](#snapshot-item-view)
**Additional**
- [Login](#login-view)
- [Task](#task-view) WIP
- [Refresh](#refresh-view)
- [Cookie](#cookie-view)
- [Search](#search-view)
- [Watched](#watched-view)
- [Ping](#ping-view)
## Authentication
API token will get automatically created, accessible on the settings page. Token needs to be passed as an authorization header with every request. Additionally session based authentication is enabled too: When you are logged into your TubeArchivist instance, you'll have access to the api in the browser for testing.
Curl example:
```shell
curl -v /api/video/<video-id>/ \
-H "Authorization: Token xxxxxxxxxx"
```
Python requests example:
```python
import requests
url = "/api/video/<video-id>/"
headers = {"Authorization": "Token xxxxxxxxxx"}
response = requests.get(url, headers=headers)
```
## Pagination
The list views return a paginate object with the following keys:
- page_size: *int* current page size set in config
- page_from: *int* first result idx
- prev_pages: *array of ints* of previous pages, if available
- current_page: *int* current page from query
- max_hits: *bool* if max of 10k results is reached
- params: *str* additional url encoded query parameters
- last_page: *int* of last page link
- next_pages: *array of ints* of next pages
- total_hits: *int* total results
Pass page number as a query parameter: `page=2`. Defaults to *0*, `page=1` is redundant and falls back to *0*. If a page query doesn't return any results, you'll get `HTTP 404 Not Found`.
## Video List View
/api/video/
## Video Item View
GET: /api/video/\<video_id>/
DELETE: /api/video/\<video_id>/
## Video Comment View
/api/video/\<video_id>/comment/
## Video Similar View
/api/video/\<video_id>/similar/
## Video Progress View
/api/video/\<video_id>/progress/
Progress is stored for each user.
### Get last player position of a video
GET /api/video/\<video_id>/progress/
```json
{
"youtube_id": "<video_id>",
"user_id": 1,
"position": 100
}
```
### Post player position of video
POST /api/video/\<video_id>/progress/
```json
{
"position": 100
}
```
### Delete player position of video
DELETE /api/video/\<video_id>/progress/
## Sponsor Block View
/api/video/\<video_id>/sponsor/
Integrate with sponsorblock
### Get list of segments
GET /api/video/\<video_id>/sponsor/
### Vote on existing segment
**This only simulates the request**
POST /api/video/\<video_id>/sponsor/
```json
{
"vote": {
"uuid": "<uuid>",
"yourVote": 1
}
}
```
yourVote needs to be *int*: 0 for downvote, 1 for upvote, 20 to undo vote
### Create new segment
**This only simulates the request**
POST /api/video/\<video_id>/sponsor/
```json
{
"segment": {
"startTime": 5,
"endTime": 10
}
}
```
Timestamps either *int* or *float*, end time can't be before start time.
## Channel List View
/api/channel/
Parameter:
- filter: subscribed
### Subscribe to a list of channels
POST /api/channel/
```json
{
"data": [
{"channel_id": "UC9-y-6csu5WGm29I7JiwpnA", "channel_subscribed": true}
]
}
```
## Channel Item View
GET: /api/channel/\<channel_id>/
DELETE: /api/channel/\<channel_id>/
- Will delete channel with all it's videos
## Channel Videos View
/api/channel/\<channel_id>/video/
## Playlist List View
/api/playlist/
## Playlist Item View
/api/playlist/\<playlist_id>/
## Playlist Videos View
/api/playlist/\<playlist_id>/video/
## Download Queue List View
GET /api/download/
Parameter:
- filter: pending, ignore
- channel: channel-id
### Add list of videos to download queue
POST /api/download/
```json
{
"data": [
{"youtube_id": "NYj3DnI81AQ", "status": "pending"}
]
}
```
### Delete download queue items by filter
DELETE /api/download/?filter=ignore
DELETE /api/download/?filter=pending
## Download Queue Item View
GET /api/download/\<video_id>/
POST /api/download/\<video_id>/
Ignore video in download queue:
```json
{
"status": "ignore"
}
```
Add to queue previously ignored video:
```json
{
"status": "pending"
}
```
Download existing video now:
```json
{
"status": "priority"
}
```
DELETE /api/download/\<video_id>/
Forget or delete from download queue
## Snapshot List View
GET /api/snapshot/
Return snapshot config and a list of available snapshots.
```json
{
"next_exec": epoch,
"next_exec_str": "date_str",
"expire_after": "30d",
"snapshots": []
}
```
POST /api/snapshot/
Create new snapshot now, will return immediately, task will run async in the background, will return snapshot name:
```json
{
"snapshot_name": "ta_daily_<random-id>
}
```
## Snapshot Item View
GET /api/snapshot/\<snapshot-id>/
Return metadata of a single snapshot
```json
{
"id": "ta_daily_<random-id>,
"state": "SUCCESS",
"es_version": "0.0.0",
"start_date": "date_str",
"end_date": "date_str",
"end_stamp": epoch,
"duration_s": 0
}
```
GET /api/snapshot/\<snapshot-id>/
Restore this snapshot
DELETE /api/snapshot/\<snapshot-id>/
Remove this snapshot from index
## Login View
Return token and user ID for username and password:
POST /api/login/
```json
{
"username": "tubearchivist",
"password": "verysecret"
}
```
after successful login returns
```json
{
"token": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"user_id": 1
}
```
## Task View
GET /api/task/
POST /api/task/
Check if there is an ongoing task:
GET /api/task/
Returns:
```json
{
"rescan": false,
"downloading": false
}
```
Start a background task
POST /api/task/
```json
{
"run": "task_name"
}
```
List of valid task names:
- **download_pending**: Start the download queue
- **rescan_pending**: Rescan your subscriptions
## Refresh View
GET /api/refresh/
parameters:
- **type**: one of *video*, *channel*, *playlist*, optional
- **id**: item id, optional
without specifying type: return total for all queued items:
```json
{
"total_queued": 2,
"type": "all",
"state": "running"
}
```
specify type: return total items queue of this type:
```json
{
"total_queued": 2,
"type": "video",
"state": "running"
}
```
specify type *and* id to get state of item in queue:
```json
{
"total_queued": 2,
"type": "video",
"state": "in_queue",
"id": "video-id"
}
```
POST /api/refresh/
Parameter:
- extract_videos: to refresh all videos for channels/playlists, default False
Manually start a refresh task: post list of *video*, *channel*, *playlist* IDs.
```json
{
"video": ["video1", "video2", "video3"],
"channel": ["channel1", "channel2", "channel3"],
"playlist": ["playlist1", "playlist2"]
}
```
## Cookie View
Check your youtube cookie settings, *status* turns to `true` if cookie has been validated.
GET /api/cookie/
```json
{
"cookie_enabled": true,
"status": true,
"validated": <timestamp>,
"validated_str": "timestamp"
}
```
POST /api/cookie/
Send empty post request to validate cookie.
```json
{
"cookie_validated": true
}
```
PUT /api/cookie/
Send put request containing the cookie as a string:
```json
{
"cookie": "your-cookie-as-string"
}
```
Imports and validates cookie, returns on success:
```json
{
"cookie_import": "done",
"cookie_validated": true
}
```
Or returns status code 400 on failure:
```json
{
"cookie_import": "fail",
"cookie_validated": false
}
```
## Search View
GET /api/search/?query=\<query>
Returns search results from your query.
## Watched View
POST /api/watched/
Change watched state, where the `id` can be a single video, or channel/playlist to change all videos belonging to that channel/playlist.
```json
{
"id": "xxxxxxx",
"is_watched": True
}
```
## Ping View
Validate your connection with the API
GET /api/ping/
When valid returns message with user id:
```json
{
"response": "pong",
"user": 1
}
```

View File

View File

@@ -0,0 +1,3 @@
from django.contrib import admin # noqa: F401
# Register your models here.

10
tubearchivist/api/apps.py Normal file
View File

@@ -0,0 +1,10 @@
"""apps file for api package"""
from django.apps import AppConfig
class ApiConfig(AppConfig):
"""app config"""
default_auto_field = "django.db.models.BigAutoField"
name = "api"

View File

View File

@@ -0,0 +1,3 @@
"""api models"""
# from django.db import models

View File

View File

View File

@@ -0,0 +1,141 @@
"""
Functionality:
- processing search results for frontend
- this is duplicated code from home.src.frontend.searching.SearchHandler
"""
import urllib.parse
from home.src.download.thumbnails import ThumbManager
from home.src.ta.config import AppConfig
from home.src.ta.helper import date_praser
class SearchProcess:
"""process search results"""
CONFIG = AppConfig().config
CACHE_DIR = CONFIG["application"]["cache_dir"]
def __init__(self, response):
self.response = response
self.processed = False
def process(self):
"""detect type and process"""
if "_source" in self.response.keys():
# single
self.processed = self._process_result(self.response)
elif "hits" in self.response.keys():
# multiple
self.processed = []
all_sources = self.response["hits"]["hits"]
for result in all_sources:
self.processed.append(self._process_result(result))
return self.processed
def _process_result(self, result):
"""detect which type of data to process"""
index = result["_index"]
processed = False
if index == "ta_video":
processed = self._process_video(result["_source"])
if index == "ta_channel":
processed = self._process_channel(result["_source"])
if index == "ta_playlist":
processed = self._process_playlist(result["_source"])
if index == "ta_download":
processed = self._process_download(result["_source"])
if index == "ta_comment":
processed = self._process_comment(result["_source"])
return processed
@staticmethod
def _process_channel(channel_dict):
"""run on single channel"""
channel_id = channel_dict["channel_id"]
art_base = f"/cache/channels/{channel_id}"
date_str = date_praser(channel_dict["channel_last_refresh"])
channel_dict.update(
{
"channel_last_refresh": date_str,
"channel_banner_url": f"{art_base}_banner.jpg",
"channel_thumb_url": f"{art_base}_thumb.jpg",
"channel_tvart_url": False,
}
)
return dict(sorted(channel_dict.items()))
def _process_video(self, video_dict):
"""run on single video dict"""
video_id = video_dict["youtube_id"]
media_url = urllib.parse.quote(video_dict["media_url"])
vid_last_refresh = date_praser(video_dict["vid_last_refresh"])
published = date_praser(video_dict["published"])
vid_thumb_url = ThumbManager(video_id).vid_thumb_path()
channel = self._process_channel(video_dict["channel"])
if "subtitles" in video_dict:
for idx, _ in enumerate(video_dict["subtitles"]):
url = video_dict["subtitles"][idx]["media_url"]
video_dict["subtitles"][idx]["media_url"] = f"/media/{url}"
video_dict.update(
{
"channel": channel,
"media_url": f"/media/{media_url}",
"vid_last_refresh": vid_last_refresh,
"published": published,
"vid_thumb_url": f"{self.CACHE_DIR}/{vid_thumb_url}",
}
)
return dict(sorted(video_dict.items()))
@staticmethod
def _process_playlist(playlist_dict):
"""run on single playlist dict"""
playlist_id = playlist_dict["playlist_id"]
playlist_last_refresh = date_praser(
playlist_dict["playlist_last_refresh"]
)
playlist_dict.update(
{
"playlist_thumbnail": f"/cache/playlists/{playlist_id}.jpg",
"playlist_last_refresh": playlist_last_refresh,
}
)
return dict(sorted(playlist_dict.items()))
def _process_download(self, download_dict):
"""run on single download item"""
video_id = download_dict["youtube_id"]
vid_thumb_url = ThumbManager(video_id).vid_thumb_path()
published = date_praser(download_dict["published"])
download_dict.update(
{
"vid_thumb_url": f"{self.CACHE_DIR}/{vid_thumb_url}",
"published": published,
}
)
return dict(sorted(download_dict.items()))
def _process_comment(self, comment_dict):
"""run on all comments, create reply thread"""
all_comments = comment_dict["comment_comments"]
processed_comments = []
for comment in all_comments:
if comment["comment_parent"] == "root":
comment.update({"comment_replies": []})
processed_comments.append(comment)
else:
processed_comments[-1]["comment_replies"].append(comment)
return processed_comments

View File

@@ -0,0 +1,54 @@
"""
Functionality:
- process tasks from API
- validate
- handover to celery
"""
from home.src.ta.ta_redis import RedisArchivist
from home.tasks import download_pending, update_subscribed
class TaskHandler:
"""handle tasks from api"""
def __init__(self, data):
self.data = data
def run_task(self):
"""map data and run"""
task_name = self.data["run"]
try:
to_run = self.exec_map(task_name)
except KeyError as err:
print(f"invalid task name {task_name}")
raise ValueError from err
response = to_run()
response.update({"task": task_name})
return response
def exec_map(self, task_name):
"""map dict key and return function to execute"""
exec_map = {
"download_pending": self._download_pending,
"rescan_pending": self._rescan_pending,
}
return exec_map[task_name]
@staticmethod
def _rescan_pending():
"""look for new items in subscribed channels"""
print("rescan subscribed channels")
update_subscribed.delay()
return {"success": True}
@staticmethod
def _download_pending():
"""start the download queue"""
print("download pending")
running = download_pending.delay()
print("set task id: " + running.id)
RedisArchivist().set_message("dl_queue_id", running.id)
return {"success": True}

View File

@@ -0,0 +1,3 @@
from django.test import TestCase # noqa: F401
# Create your tests here.

138
tubearchivist/api/urls.py Normal file
View File

@@ -0,0 +1,138 @@
"""all api urls"""
from api.views import (
ChannelApiListView,
ChannelApiVideoView,
ChannelApiView,
CookieView,
DownloadApiListView,
DownloadApiView,
LoginApiView,
PingView,
PlaylistApiListView,
PlaylistApiVideoView,
PlaylistApiView,
RefreshView,
SearchView,
SnapshotApiListView,
SnapshotApiView,
TaskApiView,
VideoApiListView,
VideoApiView,
VideoCommentView,
VideoProgressView,
VideoSimilarView,
VideoSponsorView,
WatchedView,
)
from django.urls import path
urlpatterns = [
path("ping/", PingView.as_view(), name="ping"),
path("login/", LoginApiView.as_view(), name="api-login"),
path(
"video/",
VideoApiListView.as_view(),
name="api-video-list",
),
path(
"video/<slug:video_id>/",
VideoApiView.as_view(),
name="api-video",
),
path(
"video/<slug:video_id>/progress/",
VideoProgressView.as_view(),
name="api-video-progress",
),
path(
"video/<slug:video_id>/comment/",
VideoCommentView.as_view(),
name="api-video-comment",
),
path(
"video/<slug:video_id>/similar/",
VideoSimilarView.as_view(),
name="api-video-similar",
),
path(
"video/<slug:video_id>/sponsor/",
VideoSponsorView.as_view(),
name="api-video-sponsor",
),
path(
"channel/",
ChannelApiListView.as_view(),
name="api-channel-list",
),
path(
"channel/<slug:channel_id>/",
ChannelApiView.as_view(),
name="api-channel",
),
path(
"channel/<slug:channel_id>/video/",
ChannelApiVideoView.as_view(),
name="api-channel-video",
),
path(
"playlist/",
PlaylistApiListView.as_view(),
name="api-playlist-list",
),
path(
"playlist/<slug:playlist_id>/",
PlaylistApiView.as_view(),
name="api-playlist",
),
path(
"playlist/<slug:playlist_id>/video/",
PlaylistApiVideoView.as_view(),
name="api-playlist-video",
),
path(
"download/",
DownloadApiListView.as_view(),
name="api-download-list",
),
path(
"download/<slug:video_id>/",
DownloadApiView.as_view(),
name="api-download",
),
path(
"refresh/",
RefreshView.as_view(),
name="api-refresh",
),
path(
"task/",
TaskApiView.as_view(),
name="api-task",
),
path(
"snapshot/",
SnapshotApiListView.as_view(),
name="api-snapshot-list",
),
path(
"snapshot/<slug:snapshot_id>/",
SnapshotApiView.as_view(),
name="api-snapshot",
),
path(
"cookie/",
CookieView.as_view(),
name="api-cookie",
),
path(
"watched/",
WatchedView.as_view(),
name="api-watched",
),
path(
"search/",
SearchView.as_view(),
name="api-search",
),
]

762
tubearchivist/api/views.py Normal file
View File

@@ -0,0 +1,762 @@
"""all API views"""
from api.src.search_processor import SearchProcess
from api.src.task_processor import TaskHandler
from home.src.download.queue import PendingInteract
from home.src.download.yt_dlp_base import CookieHandler
from home.src.es.connect import ElasticWrap
from home.src.es.snapshot import ElasticSnapshot
from home.src.frontend.searching import SearchForm
from home.src.frontend.watched import WatchState
from home.src.index.channel import YoutubeChannel
from home.src.index.generic import Pagination
from home.src.index.reindex import ReindexProgress
from home.src.index.video import SponsorBlock, YoutubeVideo
from home.src.ta.config import AppConfig
from home.src.ta.ta_redis import RedisArchivist, RedisQueue
from home.src.ta.urlparser import Parser
from home.tasks import check_reindex, download_single, extrac_dl, subscribe_to
from rest_framework.authentication import (
SessionAuthentication,
TokenAuthentication,
)
from rest_framework.authtoken.models import Token
from rest_framework.authtoken.views import ObtainAuthToken
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
class ApiBaseView(APIView):
"""base view to inherit from"""
authentication_classes = [SessionAuthentication, TokenAuthentication]
permission_classes = [IsAuthenticated]
search_base = False
data = False
def __init__(self):
super().__init__()
self.response = {"data": False, "config": AppConfig().config}
self.data = {"query": {"match_all": {}}}
self.status_code = False
self.context = False
self.pagination_handler = False
def get_document(self, document_id):
"""get single document from es"""
path = f"{self.search_base}{document_id}"
response, status_code = ElasticWrap(path).get()
try:
self.response["data"] = SearchProcess(response).process()
except KeyError:
print(f"item not found: {document_id}")
self.response["data"] = False
self.status_code = status_code
def initiate_pagination(self, request):
"""set initial pagination values"""
self.pagination_handler = Pagination(request)
self.data.update(
{
"size": self.pagination_handler.pagination["page_size"],
"from": self.pagination_handler.pagination["page_from"],
}
)
def get_document_list(self, request, pagination=True):
"""get a list of results"""
if pagination:
self.initiate_pagination(request)
es_handler = ElasticWrap(self.search_base)
response, status_code = es_handler.get(data=self.data)
self.response["data"] = SearchProcess(response).process()
if self.response["data"]:
self.status_code = status_code
else:
self.status_code = 404
if pagination:
self.pagination_handler.validate(
response["hits"]["total"]["value"]
)
self.response["paginate"] = self.pagination_handler.pagination
class VideoApiView(ApiBaseView):
"""resolves to /api/video/<video_id>/
GET: returns metadata dict of video
"""
search_base = "ta_video/_doc/"
def get(self, request, video_id):
# pylint: disable=unused-argument
"""get request"""
self.get_document(video_id)
return Response(self.response, status=self.status_code)
def delete(self, request, video_id):
# pylint: disable=unused-argument
"""delete single video"""
message = {"video": video_id}
try:
YoutubeVideo(video_id).delete_media_file()
status_code = 200
message.update({"state": "delete"})
except FileNotFoundError:
status_code = 404
message.update({"state": "not found"})
return Response(message, status=status_code)
class VideoApiListView(ApiBaseView):
"""resolves to /api/video/
GET: returns list of videos
"""
search_base = "ta_video/_search/"
def get(self, request):
"""get request"""
self.data.update({"sort": [{"published": {"order": "desc"}}]})
self.get_document_list(request)
return Response(self.response)
class VideoProgressView(ApiBaseView):
"""resolves to /api/video/<video_id>/
handle progress status for video
"""
def get(self, request, video_id):
"""get progress for a single video"""
user_id = request.user.id
key = f"{user_id}:progress:{video_id}"
video_progress = RedisArchivist().get_message(key)
position = video_progress.get("position", 0)
self.response = {
"youtube_id": video_id,
"user_id": user_id,
"position": position,
}
return Response(self.response)
def post(self, request, video_id):
"""set progress position in redis"""
position = request.data.get("position", 0)
key = f"{request.user.id}:progress:{video_id}"
message = {"position": position, "youtube_id": video_id}
RedisArchivist().set_message(key, message)
self.response = request.data
return Response(self.response)
def delete(self, request, video_id):
"""delete progress position"""
key = f"{request.user.id}:progress:{video_id}"
RedisArchivist().del_message(key)
self.response = {"progress-reset": video_id}
return Response(self.response)
class VideoCommentView(ApiBaseView):
"""resolves to /api/video/<video_id>/comment/
handle video comments
GET: return all comments from video with reply threads
"""
search_base = "ta_comment/_doc/"
def get(self, request, video_id):
"""get video comments"""
# pylint: disable=unused-argument
self.get_document(video_id)
return Response(self.response, status=self.status_code)
class VideoSimilarView(ApiBaseView):
"""resolves to /api/video/<video-id>/similar/
GET: return max 3 videos similar to this
"""
search_base = "ta_video/_search/"
def get(self, request, video_id):
"""get similar videos"""
self.data = {
"size": 6,
"query": {
"more_like_this": {
"fields": ["tags", "title"],
"like": {"_id": video_id},
"min_term_freq": 1,
"max_query_terms": 25,
}
},
}
self.get_document_list(request, pagination=False)
return Response(self.response, status=self.status_code)
class VideoSponsorView(ApiBaseView):
"""resolves to /api/video/<video_id>/sponsor/
handle sponsor block integration
"""
search_base = "ta_video/_doc/"
def get(self, request, video_id):
"""get sponsor info"""
# pylint: disable=unused-argument
self.get_document(video_id)
sponsorblock = self.response["data"].get("sponsorblock")
return Response(sponsorblock)
def post(self, request, video_id):
"""post verification and timestamps"""
if "segment" in request.data:
response, status_code = self._create_segment(request, video_id)
elif "vote" in request.data:
response, status_code = self._vote_on_segment(request)
return Response(response, status=status_code)
@staticmethod
def _create_segment(request, video_id):
"""create segment in API"""
start_time = request.data["segment"]["startTime"]
end_time = request.data["segment"]["endTime"]
response, status_code = SponsorBlock(request.user.id).post_timestamps(
video_id, start_time, end_time
)
return response, status_code
@staticmethod
def _vote_on_segment(request):
"""validate on existing segment"""
user_id = request.user.id
uuid = request.data["vote"]["uuid"]
vote = request.data["vote"]["yourVote"]
response, status_code = SponsorBlock(user_id).vote_on_segment(
uuid, vote
)
return response, status_code
class ChannelApiView(ApiBaseView):
"""resolves to /api/channel/<channel_id>/
GET: returns metadata dict of channel
"""
search_base = "ta_channel/_doc/"
def get(self, request, channel_id):
# pylint: disable=unused-argument
"""get request"""
self.get_document(channel_id)
return Response(self.response, status=self.status_code)
def delete(self, request, channel_id):
# pylint: disable=unused-argument
"""delete channel"""
message = {"channel": channel_id}
try:
YoutubeChannel(channel_id).delete_channel()
status_code = 200
message.update({"state": "delete"})
except FileNotFoundError:
status_code = 404
message.update({"state": "not found"})
return Response(message, status=status_code)
class ChannelApiListView(ApiBaseView):
"""resolves to /api/channel/
GET: returns list of channels
POST: edit a list of channels
"""
search_base = "ta_channel/_search/"
valid_filter = ["subscribed"]
def get(self, request):
"""get request"""
self.data.update(
{"sort": [{"channel_name.keyword": {"order": "asc"}}]}
)
query_filter = request.GET.get("filter", False)
must_list = []
if query_filter:
if query_filter not in self.valid_filter:
message = f"invalid url query filder: {query_filter}"
print(message)
return Response({"message": message}, status=400)
must_list.append({"term": {"channel_subscribed": {"value": True}}})
self.data["query"] = {"bool": {"must": must_list}}
self.get_document_list(request)
return Response(self.response)
@staticmethod
def post(request):
"""subscribe to list of channels"""
data = request.data
try:
to_add = data["data"]
except KeyError:
message = "missing expected data key"
print(message)
return Response({"message": message}, status=400)
pending = [i["channel_id"] for i in to_add if i["channel_subscribed"]]
url_str = " ".join(pending)
subscribe_to.delay(url_str)
return Response(data)
class ChannelApiVideoView(ApiBaseView):
"""resolves to /api/channel/<channel-id>/video
GET: returns a list of videos of channel
"""
search_base = "ta_video/_search/"
def get(self, request, channel_id):
"""handle get request"""
self.data.update(
{
"query": {
"term": {"channel.channel_id": {"value": channel_id}}
},
"sort": [{"published": {"order": "desc"}}],
}
)
self.get_document_list(request)
return Response(self.response, status=self.status_code)
class PlaylistApiListView(ApiBaseView):
"""resolves to /api/playlist/
GET: returns list of indexed playlists
"""
search_base = "ta_playlist/_search/"
def get(self, request):
"""handle get request"""
self.data.update(
{"sort": [{"playlist_name.keyword": {"order": "asc"}}]}
)
self.get_document_list(request)
return Response(self.response)
class PlaylistApiView(ApiBaseView):
"""resolves to /api/playlist/<playlist_id>/
GET: returns metadata dict of playlist
"""
search_base = "ta_playlist/_doc/"
def get(self, request, playlist_id):
# pylint: disable=unused-argument
"""get request"""
self.get_document(playlist_id)
return Response(self.response, status=self.status_code)
class PlaylistApiVideoView(ApiBaseView):
"""resolves to /api/playlist/<playlist_id>/video
GET: returns list of videos in playlist
"""
search_base = "ta_video/_search/"
def get(self, request, playlist_id):
"""handle get request"""
self.data["query"] = {
"term": {"playlist.keyword": {"value": playlist_id}}
}
self.data.update({"sort": [{"published": {"order": "desc"}}]})
self.get_document_list(request)
return Response(self.response, status=self.status_code)
class DownloadApiView(ApiBaseView):
"""resolves to /api/download/<video_id>/
GET: returns metadata dict of an item in the download queue
POST: update status of item to pending or ignore
DELETE: forget from download queue
"""
search_base = "ta_download/_doc/"
valid_status = ["pending", "ignore", "priority"]
def get(self, request, video_id):
# pylint: disable=unused-argument
"""get request"""
self.get_document(video_id)
return Response(self.response, status=self.status_code)
def post(self, request, video_id):
"""post to video to change status"""
item_status = request.data.get("status")
if item_status not in self.valid_status:
message = f"{video_id}: invalid status {item_status}"
print(message)
return Response({"message": message}, status=400)
pending_video, status_code = PendingInteract(video_id).get_item()
if status_code == 404:
message = f"{video_id}: item not found {status_code}"
return Response({"message": message}, status=404)
print(f"{video_id}: change status to {item_status}")
if item_status == "priority":
download_single.delay(pending_video)
else:
PendingInteract(video_id, item_status).update_status()
RedisQueue(queue_name="dl_queue").clear_item(video_id)
return Response(request.data)
@staticmethod
def delete(request, video_id):
# pylint: disable=unused-argument
"""delete single video from queue"""
print(f"{video_id}: delete from queue")
PendingInteract(video_id).delete_item()
return Response({"success": True})
class DownloadApiListView(ApiBaseView):
"""resolves to /api/download/
GET: returns latest videos in the download queue
POST: add a list of videos to download queue
DELETE: remove items based on query filter
"""
search_base = "ta_download/_search/"
valid_filter = ["pending", "ignore"]
def get(self, request):
"""get request"""
query_filter = request.GET.get("filter", False)
self.data.update({"sort": [{"timestamp": {"order": "asc"}}]})
must_list = []
if query_filter:
if query_filter not in self.valid_filter:
message = f"invalid url query filder: {query_filter}"
print(message)
return Response({"message": message}, status=400)
must_list.append({"term": {"status": {"value": query_filter}}})
filter_channel = request.GET.get("channel", False)
if filter_channel:
must_list.append(
{"term": {"channel_id": {"value": filter_channel}}}
)
self.data["query"] = {"bool": {"must": must_list}}
self.get_document_list(request)
return Response(self.response)
@staticmethod
def post(request):
"""add list of videos to download queue"""
data = request.data
try:
to_add = data["data"]
except KeyError:
message = "missing expected data key"
print(message)
return Response({"message": message}, status=400)
pending = [i["youtube_id"] for i in to_add if i["status"] == "pending"]
url_str = " ".join(pending)
try:
youtube_ids = Parser(url_str).parse()
except ValueError:
message = f"failed to parse: {url_str}"
print(message)
return Response({"message": message}, status=400)
extrac_dl.delay(youtube_ids)
return Response(data)
def delete(self, request):
"""delete download queue"""
query_filter = request.GET.get("filter", False)
if query_filter not in self.valid_filter:
message = f"invalid url query filter: {query_filter}"
print(message)
return Response({"message": message}, status=400)
message = f"delete queue by status: {query_filter}"
print(message)
PendingInteract(status=query_filter).delete_by_status()
return Response({"message": message})
class PingView(ApiBaseView):
"""resolves to /api/ping/
GET: test your connection
"""
@staticmethod
def get(request):
"""get pong"""
data = {"response": "pong", "user": request.user.id}
return Response(data)
class LoginApiView(ObtainAuthToken):
"""resolves to /api/login/
POST: return token and username after successful login
"""
def post(self, request, *args, **kwargs):
"""post data"""
# pylint: disable=no-member
serializer = self.serializer_class(
data=request.data, context={"request": request}
)
serializer.is_valid(raise_exception=True)
user = serializer.validated_data["user"]
token, _ = Token.objects.get_or_create(user=user)
print(f"returning token for user with id {user.pk}")
return Response({"token": token.key, "user_id": user.pk})
class TaskApiView(ApiBaseView):
"""resolves to /api/task/
GET: check if ongoing background task
POST: start a new background task
"""
@staticmethod
def get(request):
"""handle get request"""
# pylint: disable=unused-argument
response = {"rescan": False, "downloading": False}
for key in response.keys():
response[key] = RedisArchivist().is_locked(key)
return Response(response)
def post(self, request):
"""handle post request"""
response = TaskHandler(request.data).run_task()
return Response(response)
class SnapshotApiListView(ApiBaseView):
"""resolves to /api/snapshot/
GET: returns snashot config plus list of existing snapshots
POST: take snapshot now
"""
@staticmethod
def get(request):
"""handle get request"""
# pylint: disable=unused-argument
snapshots = ElasticSnapshot().get_snapshot_stats()
return Response(snapshots)
@staticmethod
def post(request):
"""take snapshot now with post request"""
# pylint: disable=unused-argument
response = ElasticSnapshot().take_snapshot_now()
return Response(response)
class SnapshotApiView(ApiBaseView):
"""resolves to /api/snapshot/<snapshot-id>/
GET: return a single snapshot
POST: restore snapshot
DELETE: delete a snapshot
"""
@staticmethod
def get(request, snapshot_id):
"""handle get request"""
# pylint: disable=unused-argument
snapshot = ElasticSnapshot().get_single_snapshot(snapshot_id)
if not snapshot:
return Response({"message": "snapshot not found"}, status=404)
return Response(snapshot)
@staticmethod
def post(request, snapshot_id):
"""restore snapshot with post request"""
# pylint: disable=unused-argument
response = ElasticSnapshot().restore_all(snapshot_id)
if not response:
message = {"message": "failed to restore snapshot"}
return Response(message, status=400)
return Response(response)
@staticmethod
def delete(request, snapshot_id):
"""delete snapshot from index"""
# pylint: disable=unused-argument
response = ElasticSnapshot().delete_single_snapshot(snapshot_id)
if not response:
message = {"message": "failed to delete snapshot"}
return Response(message, status=400)
return Response(response)
class RefreshView(ApiBaseView):
"""resolves to /api/refresh/
GET: get refresh progress
POST: start a manual refresh task
"""
def get(self, request):
"""handle get request"""
request_type = request.GET.get("type")
request_id = request.GET.get("id")
if request_id and not request_type:
return Response({"status": "Bad Request"}, status=400)
try:
progress = ReindexProgress(
request_type=request_type, request_id=request_id
).get_progress()
except ValueError:
return Response({"status": "Bad Request"}, status=400)
return Response(progress)
def post(self, request):
"""handle post request"""
data = request.data
extract_videos = bool(request.GET.get("extract_videos", False))
check_reindex.delay(data=data, extract_videos=extract_videos)
return Response(data)
class CookieView(ApiBaseView):
"""resolves to /api/cookie/
GET: check if cookie is enabled
POST: verify validity of cookie
PUT: import cookie
"""
@staticmethod
def get(request):
"""handle get request"""
# pylint: disable=unused-argument
config = AppConfig().config
valid = RedisArchivist().get_message("cookie:valid")
response = {"cookie_enabled": config["downloads"]["cookie_import"]}
response.update(valid)
return Response(response)
@staticmethod
def post(request):
"""handle post request"""
# pylint: disable=unused-argument
config = AppConfig().config
validated = CookieHandler(config).validate()
return Response({"cookie_validated": validated})
@staticmethod
def put(request):
"""handle put request"""
# pylint: disable=unused-argument
config = AppConfig().config
cookie = request.data.get("cookie")
if not cookie:
message = "missing cookie key in request data"
print(message)
return Response({"message": message}, status=400)
print(f"cookie preview:\n\n{cookie[:300]}")
handler = CookieHandler(config)
handler.set_cookie(cookie)
validated = handler.validate()
if not validated:
handler.revoke()
message = {"cookie_import": "fail", "cookie_validated": validated}
print(f"cookie: {message}")
return Response({"message": message}, status=400)
message = {"cookie_import": "done", "cookie_validated": validated}
return Response(message)
class WatchedView(ApiBaseView):
"""resolves to /api/watched/
POST: change watched state of video, channel or playlist
"""
def post(self, request):
"""change watched state"""
youtube_id = request.data.get("id")
is_watched = request.data.get("is_watched")
if not youtube_id or is_watched is None:
message = {"message": "missing id or is_watched"}
return Response(message, status=400)
WatchState(youtube_id, is_watched).change()
return Response({"message": "success"}, status=200)
class SearchView(ApiBaseView):
"""resolves to /api/search/
GET: run a search with the string in the ?query parameter
"""
@staticmethod
def get(request):
"""handle get request
search through all indexes"""
search_query = request.GET.get("query", None)
if search_query is None:
return Response(
{"message": "no search query specified"}, status=400
)
search_results = SearchForm().multi_search(search_query)
return Response(search_results)

View File

@@ -11,6 +11,6 @@ import os
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
application = get_asgi_application()

View File

@@ -0,0 +1,140 @@
"""
Functionality:
- check that all connections are working
"""
from time import sleep
import requests
from django.core.management.base import BaseCommand, CommandError
from home.src.es.connect import ElasticWrap
from home.src.ta.ta_redis import RedisArchivist
TOPIC = """
#######################
# Connection check #
#######################
"""
class Command(BaseCommand):
"""command framework"""
TIMEOUT = 120
MIN_MAJOR, MAX_MAJOR = 8, 8
MIN_MINOR = 0
# pylint: disable=no-member
help = "Check connections"
def handle(self, *args, **options):
"""run all commands"""
self.stdout.write(TOPIC)
self._redis_connection_check()
self._redis_config_set()
self._es_connection_check()
self._es_version_check()
self._es_path_check()
def _redis_connection_check(self):
"""check ir redis connection is established"""
self.stdout.write("[1] connect to Redis")
redis_conn = RedisArchivist().conn
for _ in range(5):
sleep(2)
try:
pong = redis_conn.execute_command("PING")
if pong:
self.stdout.write(
self.style.SUCCESS(" ✓ Redis connection verified")
)
return
except Exception: # pylint: disable=broad-except
self.stdout.write(" ... retry Redis connection")
message = " 🗙 Redis connection failed"
self.stdout.write(self.style.ERROR(f"{message}"))
RedisArchivist().exec("PING")
raise CommandError(message)
def _redis_config_set(self):
"""set config for redis if not set already"""
self.stdout.write("[2] set Redis config")
redis_conn = RedisArchivist().conn
timeout_is = int(redis_conn.config_get("timeout").get("timeout"))
if not timeout_is:
redis_conn.config_set("timeout", 3600)
self.stdout.write(self.style.SUCCESS(" ✓ Redis config set"))
def _es_connection_check(self):
"""wait for elasticsearch connection"""
self.stdout.write("[3] connect to Elastic Search")
total = self.TIMEOUT // 5
for i in range(total):
sleep(5)
self.stdout.write(f" ... waiting for ES [{i}/{total}]")
try:
_, status_code = ElasticWrap("/").get(
timeout=1, print_error=False
)
except requests.exceptions.ConnectionError:
continue
if status_code and status_code == 200:
self.stdout.write(
self.style.SUCCESS(" ✓ ES connection established")
)
return
response, status_code = ElasticWrap("/").get(
timeout=1, print_error=False
)
message = " 🗙 ES connection failed"
self.stdout.write(self.style.ERROR(f"{message}"))
self.stdout.write(f" error message: {response}")
self.stdout.write(f" status code: {status_code}")
raise CommandError(message)
def _es_version_check(self):
"""check for minimal elasticsearch version"""
self.stdout.write("[4] Elastic Search version check")
response, _ = ElasticWrap("/").get()
version = response["version"]["number"]
major = int(version.split(".")[0])
if self.MIN_MAJOR <= major <= self.MAX_MAJOR:
self.stdout.write(
self.style.SUCCESS(" ✓ ES version check passed")
)
return
message = (
" 🗙 ES connection failed. "
+ f"Expected {self.MIN_MAJOR}.{self.MIN_MINOR} but got {version}"
)
self.stdout.write(self.style.ERROR(f"{message}"))
raise CommandError(message)
def _es_path_check(self):
"""check that path.repo var is set"""
self.stdout.write("[5] check ES path.repo env var")
response, _ = ElasticWrap("_nodes/_all/settings").get()
for node in response["nodes"].values():
if node["settings"]["path"].get("repo"):
self.stdout.write(
self.style.SUCCESS(" ✓ path.repo env var is set")
)
return
message = (
" 🗙 path.repo env var not found. "
+ "set the following env var to the ES container:\n"
+ " path.repo=/usr/share/elasticsearch/data/snapshot"
)
self.stdout.write(self.style.ERROR(f"{message}"))
raise CommandError(message)

View File

@@ -0,0 +1,195 @@
"""
Functionality:
- Check environment at startup
- Process config file overwrites from env var
- Stop startup on error
- python management.py ta_envcheck
"""
import os
import re
from django.core.management.base import BaseCommand, CommandError
from home.models import Account
LOGO = """
.... .....
...'',;:cc,. .;::;;,'...
..,;:cccllclc, .:ccllllcc;,..
..,:cllcc:;,'.',. ....'',;ccllc:,..
..;cllc:,'.. ...,:cccc:'.
.;cccc;.. ..,:ccc:'.
.ckkkOkxollllllllllllc. .,:::;. .,cclc;
.:0MMMMMMMMMMMMMMMMMMMX: .cNMMMWx. .;clc:
.;lOXK0000KNMMMMX00000KO; ;KMMMMMNl. .;ccl:,.
.;:c:'.....kMMMNo........ 'OMMMWMMMK: '::;;'.
....... .xMMMNl .dWMMXdOMMMO' ........
.:cc:;. .xMMMNc .lNMMNo.:XMMWx. .:cl:.
.:llc,. .:xxxd, ;KMMMk. .oWMMNl. .:llc'
.cll:. .;:;;:::,. 'OMMMK:';''kWMMK: .;llc,
.cll:. .,;;;;;;,. .,xWMMNl.:l:.;KMMMO' .;llc'
.:llc. .cOOOk; .lKNMMWx..:l:..lNMMWx. .:llc'
.;lcc,. .xMMMNc :KMMMM0, .:lc. .xWMMNl.'ccl:.
.cllc. .xMMMNc 'OMMMMXc...:lc...,0MMMKl:lcc,.
.,ccl:. .xMMMNc .xWMMMWo.,;;:lc;;;.cXMMMXdcc;.
.,clc:. .xMMMNc .lNMMMWk. .':clc:,. .dWMMW0o;.
.,clcc,. .ckkkx; .okkkOx, .';,. 'kKKK0l.
.':lcc:'..... . .. ..,;cllc,.
.,cclc,.... ....;clc;..
..,:,..,c:'.. ...';:,..,:,.
....:lcccc:;,'''.....'',;;:clllc,....
.'',;:cllllllccccclllllcc:,'..
...'',,;;;;;;;;;,''...
.....
"""
TOPIC = """
#######################
# Environment Setup #
#######################
"""
EXPECTED_ENV_VARS = [
"TA_USERNAME",
"TA_PASSWORD",
"ELASTIC_PASSWORD",
"ES_URL",
"TA_HOST",
]
INST = "https://github.com/tubearchivist/tubearchivist#installing-and-updating"
NGINX = "/etc/nginx/sites-available/default"
UWSGI = "/app/uwsgi.ini"
class Command(BaseCommand):
"""command framework"""
# pylint: disable=no-member
help = "Check environment before startup"
def handle(self, *args, **options):
"""run all commands"""
self.stdout.write(LOGO)
self.stdout.write(TOPIC)
self._expected_vars()
self._elastic_user_overwrite()
self._ta_port_overwrite()
self._ta_uwsgi_overwrite()
self._enable_cast_overwrite()
self._create_superuser()
def _expected_vars(self):
"""check if expected env vars are set"""
self.stdout.write("[1] checking expected env vars")
env = os.environ
for var in EXPECTED_ENV_VARS:
if not env.get(var):
message = f" 🗙 expected env var {var} not set\n {INST}"
self.stdout.write(self.style.ERROR(message))
raise CommandError(message)
message = " ✓ all expected env vars are set"
self.stdout.write(self.style.SUCCESS(message))
def _elastic_user_overwrite(self):
"""check for ELASTIC_USER overwrite"""
self.stdout.write("[2] set default ES user")
if not os.environ.get("ELASTIC_USER"):
os.environ.setdefault("ELASTIC_USER", "elastic")
env = os.environ.get("ELASTIC_USER")
self.stdout.write(self.style.SUCCESS(f" ✓ ES user is set to {env}"))
def _ta_port_overwrite(self):
"""set TA_PORT overwrite for nginx"""
self.stdout.write("[3] check TA_PORT overwrite")
overwrite = os.environ.get("TA_PORT")
if not overwrite:
self.stdout.write(self.style.SUCCESS(" TA_PORT is not set"))
return
regex = re.compile(r"listen [0-9]{1,5}")
changed = file_overwrite(NGINX, regex, overwrite)
if changed:
message = f" ✓ TA_PORT changed to {overwrite}"
else:
message = f" ✓ TA_PORT already set to {overwrite}"
self.stdout.write(self.style.SUCCESS(message))
def _ta_uwsgi_overwrite(self):
"""set TA_UWSGI_PORT overwrite"""
self.stdout.write("[4] check TA_UWSGI_PORT overwrite")
overwrite = os.environ.get("TA_UWSGI_PORT")
if not overwrite:
message = " TA_UWSGI_PORT is not set"
self.stdout.write(self.style.SUCCESS(message))
return
# nginx
regex = re.compile(r"uwsgi_pass localhost:[0-9]{1,5}")
to_overwrite = f"uwsgi_pass localhost:{overwrite}"
changed = file_overwrite(NGINX, regex, to_overwrite)
# uwsgi
regex = re.compile(r"socket = :[0-9]{1,5}")
to_overwrite = f"socket = :{overwrite}"
changed = file_overwrite(UWSGI, regex, to_overwrite)
if changed:
message = f" ✓ TA_UWSGI_PORT changed to {overwrite}"
else:
message = f" ✓ TA_UWSGI_PORT already set to {overwrite}"
self.stdout.write(self.style.SUCCESS(message))
def _enable_cast_overwrite(self):
"""cast workaround, remove auth for static files in nginx"""
self.stdout.write("[5] check ENABLE_CAST overwrite")
overwrite = os.environ.get("ENABLE_CAST")
if not overwrite:
self.stdout.write(self.style.SUCCESS(" ENABLE_CAST is not set"))
return
regex = re.compile(r"[^\S\r\n]*auth_request /api/ping/;\n")
changed = file_overwrite(NGINX, regex, "")
if changed:
message = " ✓ process nginx to enable Cast"
else:
message = " ✓ Cast is already enabled in nginx"
self.stdout.write(self.style.SUCCESS(message))
def _create_superuser(self):
"""create superuser if not exist"""
self.stdout.write("[6] create superuser")
is_created = Account.objects.filter(is_superuser=True)
if is_created:
message = " superuser already created"
self.stdout.write(self.style.SUCCESS(message))
return
name = os.environ.get("TA_USERNAME")
password = os.environ.get("TA_PASSWORD")
Account.objects.create_superuser(name, password)
message = f" ✓ new superuser with name {name} created"
self.stdout.write(self.style.SUCCESS(message))
def file_overwrite(file_path, regex, overwrite):
"""change file content from old to overwrite, return true when changed"""
with open(file_path, "r", encoding="utf-8") as f:
file_content = f.read()
changed = re.sub(regex, overwrite, file_content)
if changed == file_content:
return False
with open(file_path, "w", encoding="utf-8") as f:
f.write(changed)
return True

View File

@@ -0,0 +1,171 @@
"""
Functionality:
- Application startup
- Apply migrations
"""
import os
from django.core.management.base import BaseCommand, CommandError
from home.src.es.connect import ElasticWrap
from home.src.es.index_setup import ElasitIndexWrap
from home.src.es.snapshot import ElasticSnapshot
from home.src.ta.config import AppConfig, ReleaseVersion
from home.src.ta.helper import clear_dl_cache
from home.src.ta.ta_redis import RedisArchivist
TOPIC = """
#######################
# Application Start #
#######################
"""
class Command(BaseCommand):
"""command framework"""
# pylint: disable=no-member
def handle(self, *args, **options):
"""run all commands"""
self.stdout.write(TOPIC)
self._sync_redis_state()
self._make_folders()
self._release_locks()
self._clear_dl_cache()
self._version_check()
self._mig_index_setup()
self._mig_snapshot_check()
self._mig_set_vid_type()
def _sync_redis_state(self):
"""make sure redis gets new config.json values"""
self.stdout.write("[1] set new config.json values")
needs_update = AppConfig().load_new_defaults()
if needs_update:
self.stdout.write(
self.style.SUCCESS(" ✓ new config values set")
)
else:
self.stdout.write(self.style.SUCCESS(" no new config values"))
def _make_folders(self):
"""make expected cache folders"""
self.stdout.write("[2] create expected cache folders")
folders = [
"backup",
"channels",
"download",
"import",
"playlists",
"videos",
]
cache_dir = AppConfig().config["application"]["cache_dir"]
for folder in folders:
folder_path = os.path.join(cache_dir, folder)
os.makedirs(folder_path, exist_ok=True)
self.stdout.write(self.style.SUCCESS(" ✓ expected folders created"))
def _release_locks(self):
"""make sure there are no leftover locks set in redis"""
self.stdout.write("[3] clear leftover locks in redis")
all_locks = [
"dl_queue_id",
"dl_queue",
"downloading",
"manual_import",
"reindex",
"rescan",
"run_backup",
"startup_check",
]
redis_con = RedisArchivist()
has_changed = False
for lock in all_locks:
if redis_con.del_message(lock):
self.stdout.write(
self.style.SUCCESS(f" ✓ cleared lock {lock}")
)
has_changed = True
if not has_changed:
self.stdout.write(self.style.SUCCESS(" no locks found"))
def _clear_dl_cache(self):
"""clear leftover files from dl cache"""
self.stdout.write("[4] clear leftover files from dl cache")
config = AppConfig().config
leftover_files = clear_dl_cache(config)
if leftover_files:
self.stdout.write(
self.style.SUCCESS(f" ✓ cleared {leftover_files} files")
)
else:
self.stdout.write(self.style.SUCCESS(" no files found"))
def _version_check(self):
"""remove new release key if updated now"""
self.stdout.write("[5] check for first run after update")
new_version = ReleaseVersion().is_updated()
if new_version:
self.stdout.write(
self.style.SUCCESS(f" ✓ update to {new_version} completed")
)
else:
self.stdout.write(self.style.SUCCESS(" no new update found"))
def _mig_index_setup(self):
"""migration: validate index mappings"""
self.stdout.write("[MIGRATION] validate index mappings")
ElasitIndexWrap().setup()
def _mig_snapshot_check(self):
"""migration setup snapshots"""
self.stdout.write("[MIGRATION] setup snapshots")
ElasticSnapshot().setup()
def _mig_set_vid_type(self):
"""migration: update 0.3.0 to 0.3.1 set vid_type default"""
self.stdout.write("[MIGRATION] set default vid_type")
index_list = ["ta_video", "ta_download"]
data = {
"query": {
"bool": {
"should": [
{
"bool": {
"must_not": [{"exists": {"field": "vid_type"}}]
}
},
{"term": {"vid_type": {"value": "unknown"}}},
]
}
},
"script": {"source": "ctx._source['vid_type'] = 'videos'"},
}
for index_name in index_list:
path = f"{index_name}/_update_by_query"
response, status_code = ElasticWrap(path).post(data=data)
if status_code == 200:
updated = response.get("updated", 0)
if not updated:
self.stdout.write(
f" no videos needed updating in {index_name}"
)
continue
self.stdout.write(
self.style.SUCCESS(
f"{updated} videos updated in {index_name}"
)
)
else:
message = f" 🗙 {index_name} vid_type update failed"
self.stdout.write(self.style.ERROR(message))
self.stdout.write(response)
raise CommandError(message)

View File

@@ -10,8 +10,14 @@ For the full list of settings and their values, see
https://docs.djangoproject.com/en/3.2/ref/settings/
"""
import hashlib
from os import environ, path
from pathlib import Path
from os import environ
import ldap
from corsheaders.defaults import default_headers
from django_auth_ldap.config import LDAPSearch
from home.src.ta.config import AppConfig
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
@@ -20,68 +26,175 @@ BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'Fvid^aUL6LohRZz*kZFvq85B&JW&kB9o*#jdzWsdWE8*XkCLR8'
PW_HASH = hashlib.sha256(environ.get("TA_PASSWORD").encode())
SECRET_KEY = PW_HASH.hexdigest()
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = bool(environ.get('DJANGO_DEBUG'))
DEBUG = bool(environ.get("DJANGO_DEBUG"))
ALLOWED_HOSTS = ['*']
ALLOWED_HOSTS = []
if environ.get("TA_HOST"):
ALLOWED_HOSTS = [i.strip() for i in environ.get("TA_HOST").split()]
CSRF_TRUSTED_ORIGINS = []
for host in ALLOWED_HOSTS:
if host.startswith("http://") or host.startswith("https://"):
CSRF_TRUSTED_ORIGINS.append(host)
else:
CSRF_TRUSTED_ORIGINS.append(f"http://{host}")
# Application definition
INSTALLED_APPS = [
'home.apps.HomeConfig',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'whitenoise.runserver_nostatic',
'django.contrib.staticfiles',
'django.contrib.humanize'
"home.apps.HomeConfig",
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"corsheaders",
"whitenoise.runserver_nostatic",
"django.contrib.staticfiles",
"django.contrib.humanize",
"rest_framework",
"rest_framework.authtoken",
"api",
"config",
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'whitenoise.middleware.WhiteNoiseMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"corsheaders.middleware.CorsMiddleware",
"whitenoise.middleware.WhiteNoiseMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]
ROOT_URLCONF = 'config.urls'
ROOT_URLCONF = "config.urls"
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.debug",
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
],
},
},
]
WSGI_APPLICATION = 'config.wsgi.application'
WSGI_APPLICATION = "config.wsgi.application"
if bool(environ.get("TA_LDAP")):
# pylint: disable=global-at-module-level
global AUTH_LDAP_SERVER_URI
AUTH_LDAP_SERVER_URI = environ.get("TA_LDAP_SERVER_URI")
global AUTH_LDAP_BIND_DN
AUTH_LDAP_BIND_DN = environ.get("TA_LDAP_BIND_DN")
global AUTH_LDAP_BIND_PASSWORD
AUTH_LDAP_BIND_PASSWORD = environ.get("TA_LDAP_BIND_PASSWORD")
"""
Since these are new environment variables, taking the opporunity to use
more accurate env names.
Given Names are *_technically_* different from Personal names, as people
who change their names have different given names and personal names,
and they go by personal names. Additionally, "LastName" is actually
incorrect for many cultures, such as Korea, where the
family name comes first, and the personal name comes last.
But we all know people are going to try to guess at these, so still want
to include names that people will guess, hence using first/last as well.
"""
# Attribute mapping options
global AUTH_LDAP_USER_ATTR_MAP_USERNAME
AUTH_LDAP_USER_ATTR_MAP_USERNAME = (
environ.get("TA_LDAP_USER_ATTR_MAP_USERNAME")
or environ.get("TA_LDAP_USER_ATTR_MAP_UID")
or "uid"
)
global AUTH_LDAP_USER_ATTR_MAP_PERSONALNAME
AUTH_LDAP_USER_ATTR_MAP_PERSONALNAME = (
environ.get("TA_LDAP_USER_ATTR_MAP_PERSONALNAME")
or environ.get("TA_LDAP_USER_ATTR_MAP_FIRSTNAME")
or environ.get("TA_LDAP_USER_ATTR_MAP_GIVENNAME")
or "givenName"
)
global AUTH_LDAP_USER_ATTR_MAP_SURNAME
AUTH_LDAP_USER_ATTR_MAP_SURNAME = (
environ.get("TA_LDAP_USER_ATTR_MAP_SURNAME")
or environ.get("TA_LDAP_USER_ATTR_MAP_LASTNAME")
or environ.get("TA_LDAP_USER_ATTR_MAP_FAMILYNAME")
or "sn"
)
global AUTH_LDAP_USER_ATTR_MAP_EMAIL
AUTH_LDAP_USER_ATTR_MAP_EMAIL = (
environ.get("TA_LDAP_USER_ATTR_MAP_EMAIL")
or environ.get("TA_LDAP_USER_ATTR_MAP_MAIL")
or "mail"
)
global AUTH_LDAP_USER_BASE
AUTH_LDAP_USER_BASE = environ.get("TA_LDAP_USER_BASE")
global AUTH_LDAP_USER_FILTER
AUTH_LDAP_USER_FILTER = environ.get("TA_LDAP_USER_FILTER")
global AUTH_LDAP_USER_SEARCH
# pylint: disable=no-member
AUTH_LDAP_USER_SEARCH = LDAPSearch(
AUTH_LDAP_USER_BASE,
ldap.SCOPE_SUBTREE,
"(&("
+ AUTH_LDAP_USER_ATTR_MAP_USERNAME
+ "=%(user)s)"
+ AUTH_LDAP_USER_FILTER
+ ")",
)
global AUTH_LDAP_USER_ATTR_MAP
AUTH_LDAP_USER_ATTR_MAP = {
"username": AUTH_LDAP_USER_ATTR_MAP_USERNAME,
"first_name": AUTH_LDAP_USER_ATTR_MAP_PERSONALNAME,
"last_name": AUTH_LDAP_USER_ATTR_MAP_SURNAME,
"email": AUTH_LDAP_USER_ATTR_MAP_EMAIL,
}
if bool(environ.get("TA_LDAP_DISABLE_CERT_CHECK")):
global AUTH_LDAP_GLOBAL_OPTIONS
AUTH_LDAP_GLOBAL_OPTIONS = {
ldap.OPT_X_TLS_REQUIRE_CERT: ldap.OPT_X_TLS_NEVER,
}
global AUTHENTICATION_BACKENDS
AUTHENTICATION_BACKENDS = ("django_auth_ldap.backend.LDAPBackend",)
# Database
# https://docs.djangoproject.com/en/3.2/ref/settings/#databases
CACHE_DIR = AppConfig().config["application"]["cache_dir"]
DB_PATH = path.join(CACHE_DIR, "db.sqlite3")
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": DB_PATH,
}
}
@@ -91,54 +204,65 @@ DATABASES = {
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", # noqa: E501
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", # noqa: E501
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", # noqa: E501
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", # noqa: E501
},
]
AUTH_USER_MODEL = "home.Account"
# Internationalization
# https://docs.djangoproject.com/en/3.2/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
LANGUAGE_CODE = "en-us"
TIME_ZONE = environ.get("TZ") or "UTC"
USE_I18N = True
USE_L10N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/3.2/howto/static-files/
STATIC_URL = '/static/'
# STATICFILES_DIRS = [
# str(BASE_DIR.joinpath('static')),
# '/cache/'
# ]
# STATIC_URL = '/static/'
STATICFILES_DIRS = (str(BASE_DIR.joinpath('static')),)
# MEDIA_ROOT = str(BASE_DIR.joinpath('media'))
# MEDIA_URL = '/media/'
STATIC_ROOT = str(BASE_DIR.joinpath('staticfiles'))
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
STATIC_URL = "/static/"
STATICFILES_DIRS = (str(BASE_DIR.joinpath("static")),)
STATIC_ROOT = str(BASE_DIR.joinpath("staticfiles"))
STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"
# Default primary key field type
# https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
LOGIN_URL = "/login/"
LOGOUT_REDIRECT_URL = "/login/"
# Cors needed for browser extension
# background.js makes the request so HTTP_ORIGIN will be from extension
if environ.get("DISABLE_CORS"):
# disable cors
CORS_ORIGIN_ALLOW_ALL = True
else:
CORS_ALLOWED_ORIGIN_REGEXES = [
r"moz-extension://*",
r"chrome-extension://*",
]
CORS_ALLOWED_ORIGINS = ["http://localhost:3000"]
CORS_ALLOW_HEADERS = list(default_headers) + [
"mode",
]
# TA application settings
TA_UPSTREAM = "https://github.com/tubearchivist/tubearchivist"
TA_VERSION = "v0.3.3"

View File

@@ -14,9 +14,10 @@ Including another URLconf
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path, include
from django.urls import include, path
urlpatterns = [
path('', include('home.urls')),
path('admin/', admin.site.urls),
path("", include("home.urls")),
path("api/", include("api.urls")),
path("admin/", admin.site.urls),
]

View File

@@ -11,6 +11,6 @@ import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
application = get_wsgi_application()

View File

@@ -1,40 +1,5 @@
""" handle startup """
import os
from home.src.config import AppConfig
from home.src.helper import set_message
from home.src.index_management import index_check
""" handle celery startup """
from .tasks import app as celery_app
def sync_redis_state():
""" make sure redis gets the config.json values """
print('sync redis')
config = AppConfig().config
sort_order = config['archive']['sort']
set_message('sort_order', sort_order, expire=False)
hide_watched = bool(int(config['archive']['hide_watched']))
set_message('hide_watched', hide_watched, expire=False)
show_subed_only = bool(int(config['archive']['show_subed_only']))
set_message('show_subed_only', show_subed_only, expire=False)
def make_folders():
""" make needed folders here to avoid letting docker messing it up """
folders = ['download', 'channels', 'videos', 'import']
config = AppConfig().config
cache_dir = config['application']['cache_dir']
for folder in folders:
folder_path = os.path.join(cache_dir, folder)
try:
os.makedirs(folder_path)
except FileExistsError:
continue
__all__ = ('celery_app',)
make_folders()
sync_redis_state()
index_check()
__all__ = ("celery_app",)

View File

@@ -1,3 +1,36 @@
from django.contrib import admin
"""custom admin classes"""
# Register your models here.
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from .models import Account
class HomeAdmin(BaseUserAdmin):
"""register in admin page"""
list_display = ("name", "is_staff", "is_superuser")
list_filter = ("is_superuser",)
fieldsets = (
(None, {"fields": ("is_staff", "is_superuser", "password")}),
("Personal info", {"fields": ("name",)}),
("Groups", {"fields": ("groups",)}),
("Permissions", {"fields": ("user_permissions",)}),
)
add_fieldsets = (
(
None,
{"fields": ("is_staff", "is_superuser", "password1", "password2")},
),
("Personal info", {"fields": ("name",)}),
("Groups", {"fields": ("groups",)}),
("Permissions", {"fields": ("user_permissions",)}),
)
search_fields = ("name",)
ordering = ("name",)
filter_horizontal = ()
admin.site.register(Account, HomeAdmin)

View File

@@ -1,6 +1,10 @@
"""handle custom startup functions"""
from django.apps import AppConfig
class HomeConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'home'
"""call startup funcs"""
default_auto_field = "django.db.models.BigAutoField"
name = "home"

View File

@@ -1,25 +1,57 @@
{
"archive": {
"sort": "published",
"hide_watched": false,
"show_subed_only": false,
"sort_by": "published",
"sort_order": "desc",
"page_size": 12
},
"default_view": {
"home": "grid",
"channel": "list",
"downloads": "list",
"playlist": "grid",
"grid_items": 3
},
"subscriptions": {
"auto_search": false,
"auto_download": false,
"channel_size": 50
"channel_size": 50,
"live_channel_size": 50,
"shorts_channel_size": 50
},
"downloads": {
"limit_count": 5,
"limit_count": false,
"limit_speed": false,
"sleep_interval": 3,
"format": false
"autodelete_days": false,
"format": false,
"add_metadata": false,
"add_thumbnail": false,
"subtitle": false,
"subtitle_source": false,
"subtitle_index": false,
"comment_max": false,
"comment_sort": "top",
"cookie_import": false,
"throttledratelimit": false,
"integrate_ryd": false,
"integrate_sponsorblock": false
},
"application": {
"app_root": "/app",
"cache_dir": "/cache",
"videos": "/youtube",
"file_template": "%(id)s_%(title)s.mp4",
"colors": "dark"
"colors": "dark",
"enable_cast": false,
"enable_snapshot": true
},
"scheduler": {
"update_subscribed": false,
"download_pending": false,
"check_reindex": {"minute": "0", "hour": "12", "day_of_week": "*"},
"check_reindex_days": 90,
"thumbnail_check": {"minute": "0", "hour": "17", "day_of_week": "*"},
"run_backup": false,
"run_backup_rotate": 5,
"version_check": "rand-d"
}
}
}

View File

@@ -0,0 +1,35 @@
# Generated by Django 4.1.5 on 2023-02-02 06:49
from django.db import migrations, models
import home.models
class Migration(migrations.Migration):
initial = True
dependencies = [
('auth', '0012_alter_user_first_name_max_length'),
]
operations = [
migrations.CreateModel(
name='Account',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('password', models.CharField(max_length=128, verbose_name='password')),
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
('name', models.CharField(max_length=150, unique=True)),
('is_staff', models.BooleanField(default=False)),
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
],
options={
'abstract': False,
},
managers=[
('objects', home.models.AccountManager()),
],
),
]

View File

@@ -1,3 +1,53 @@
"""custom models"""
from django.contrib.auth.models import (
AbstractBaseUser,
BaseUserManager,
PermissionsMixin,
)
from django.db import models
# Create your models here.
class AccountManager(BaseUserManager):
"""manage user creation methods"""
use_in_migrations = True
def _create_user(self, name, password, **extra_fields):
"""create regular user private"""
values = [name, password]
field_value_map = dict(zip(self.model.REQUIRED_FIELDS, values))
for field_name, value in field_value_map.items():
if not value:
raise ValueError(f"The {field_name} value must be set")
user = self.model(name=name, **extra_fields)
user.set_password(password)
user.save(using=self._db)
return user
def create_user(self, name, password):
"""create regular user public"""
return self._create_user(name, password)
def create_superuser(self, name, password, **extra_fields):
"""create super user"""
extra_fields.setdefault("is_staff", True)
extra_fields.setdefault("is_superuser", True)
if extra_fields.get("is_staff") is not True:
raise ValueError("Superuser must have is_staff=True.")
if extra_fields.get("is_superuser") is not True:
raise ValueError("Superuser must have is_superuser=True.")
return self._create_user(name, password, **extra_fields)
class Account(AbstractBaseUser, PermissionsMixin):
"""handle account creation"""
name = models.CharField(max_length=150, unique=True)
is_staff = models.BooleanField(default=False)
objects = AccountManager()
USERNAME_FIELD = "name"
REQUIRED_FIELDS = ["password"]

View File

@@ -1,60 +0,0 @@
"""
Functionality:
- read and write config
- load config variables into redis
- needs to be a separate module to avoid circular import
"""
import json
import os
from home.src.helper import get_message, set_message
class AppConfig:
""" handle user settings and application variables """
def __init__(self):
self.config = self.get_config()
def get_config(self):
""" get config from default file or redis if changed """
config = self.get_config_redis()
if not config:
with open('home/config.json', 'r', encoding="utf-8") as f:
config_str = f.read()
config = json.loads(config_str)
config['application']['REDIS_HOST'] = os.environ.get('REDIS_HOST')
config['application']['es_url'] = os.environ.get('ES_URL')
config['application']['HOST_UID'] = int(os.environ.get('HOST_UID'))
config['application']['HOST_GID'] = int(os.environ.get('HOST_GID'))
return config
@staticmethod
def get_config_redis():
""" read config json set from redis to overwrite defaults """
config = get_message('config')
if not list(config.values())[0]:
return False
return config
def update_config(self, form_post):
""" update config values from settings form """
config = self.config
for key, value in form_post.items():
to_write = value[0]
if len(to_write):
if to_write == '0':
to_write = False
elif to_write.isdigit():
to_write = int(to_write)
config_dict, config_value = key.split('.')
config[config_dict][config_value] = to_write
with open('home/config.json', 'w', encoding="utf-8") as f:
f.write(json.dumps(config))
set_message('config', config, expire=False)

View File

@@ -1,505 +0,0 @@
"""
Functionality:
- handele the download queue
- manage subscriptions to channels
- downloading videos
"""
import json
import shutil
import os
from datetime import datetime
from time import sleep
import requests
import yt_dlp as youtube_dl
from home.src.index import YoutubeChannel, index_new_video
from home.src.config import AppConfig
from home.src.helper import clean_string, DurationConverter, set_message
class PendingList:
""" manage the pending videos list """
CONFIG = AppConfig().config
ES_URL = CONFIG['application']['es_url']
VIDEOS = CONFIG['application']['videos']
@staticmethod
def parse_url_list(youtube_ids):
""" extract youtube ids from list """
missing_videos = []
for entry in youtube_ids:
# notify
mess_dict = {
"status": "pending",
"level": "info",
"title": "Adding to download queue.",
"message": 'Extracting lists'
}
set_message('progress:download', mess_dict)
# extract
url = entry['url']
url_type = entry['type']
if url_type == 'video':
missing_videos.append(url)
elif url_type == 'channel':
youtube_ids = ChannelSubscription().get_last_youtube_videos(
url, limit=False
)
missing_videos = missing_videos + youtube_ids
elif url_type == 'playlist':
youtube_ids = playlist_extractor(url)
missing_videos = missing_videos + youtube_ids
return missing_videos
def add_to_pending(self, missing_videos):
""" build the bulk json data from pending """
# check if channel is indexed
channel_handler = ChannelSubscription()
all_indexed = channel_handler.get_channels(subscribed_only=False)
all_channel_ids = [i['channel_id'] for i in all_indexed]
# check if already there
all_downloaded = self.get_all_downloaded()
# loop
bulk_list = []
for video in missing_videos:
if isinstance(video, str):
youtube_id = video
elif isinstance(video, tuple):
youtube_id = video[0]
if youtube_id in all_downloaded:
# skip already downlaoded
continue
video = self.get_youtube_details(youtube_id)
# skip on download error
if not video:
continue
if video['channel_id'] in all_channel_ids:
video['channel_indexed'] = True
else:
video['channel_indexed'] = False
video['status'] = "pending"
action = {"create": {"_id": youtube_id, "_index": "ta_download"}}
bulk_list.append(json.dumps(action))
bulk_list.append(json.dumps(video))
# notify
mess_dict = {
"status": "pending",
"level": "info",
"title": "Adding to download queue.",
"message": 'Processing IDs...'
}
set_message('progress:download', mess_dict)
# add last newline
bulk_list.append('\n')
query_str = '\n'.join(bulk_list)
headers = {'Content-type': 'application/x-ndjson'}
url = self.ES_URL + '/_bulk'
request = requests.post(url, data=query_str, headers=headers)
if not request.ok:
print(request)
@staticmethod
def get_youtube_details(youtube_id):
""" get details from youtubedl for single pending video """
obs = {
'default_search': 'ytsearch',
'quiet': True,
'skip_download': True,
}
try:
vid = youtube_dl.YoutubeDL(obs).extract_info(youtube_id)
except youtube_dl.utils.DownloadError:
print('failed to extract info for: ' + youtube_id)
return False
# parse response
seconds = vid['duration']
duration_str = DurationConverter.get_str(seconds)
upload_date = vid['upload_date']
upload_dt = datetime.strptime(upload_date, "%Y%m%d")
published = upload_dt.strftime("%Y-%m-%d")
# build dict
youtube_details = {
"youtube_id": youtube_id,
"channel_name": vid['channel'],
"vid_thumb_url": vid['thumbnail'],
"title": vid['title'],
"channel_id": vid['channel_id'],
"duration": duration_str,
"published": published,
"timestamp": int(datetime.now().strftime("%s"))
}
return youtube_details
def get_all_pending(self):
""" get a list of all pending videos in ta_download """
headers = {'Content-type': 'application/json'}
# get PIT ID
url = self.ES_URL + '/ta_download/_pit?keep_alive=1m'
response = requests.post(url)
json_data = json.loads(response.text)
pit_id = json_data['id']
# query
data = {
"size": 50, "query": {"match_all": {}},
"pit": {"id": pit_id, "keep_alive": "1m"},
"sort": [{"timestamp": {"order": "desc"}}]
}
query_str = json.dumps(data)
url = self.ES_URL + '/_search'
all_pending = []
all_ignore = []
while True:
response = requests.get(url, data=query_str, headers=headers)
json_data = json.loads(response.text)
all_hits = json_data['hits']['hits']
if all_hits:
for hit in all_hits:
youtube_id = hit['_source']['youtube_id']
status = hit['_source']['status']
if status == 'pending':
all_pending.append(hit['_source'])
elif status == 'ignore':
all_ignore.append(youtube_id)
search_after = hit['sort']
# update search_after with last hit data
data['search_after'] = search_after
query_str = json.dumps(data)
else:
break
# clean up PIT
query_str = json.dumps({"id": pit_id})
requests.delete(self.ES_URL + '/_pit', data=query_str, headers=headers)
return all_pending, all_ignore
def get_all_indexed(self):
""" get a list of all videos indexed """
headers = {'Content-type': 'application/json'}
# get PIT ID
url = self.ES_URL + '/ta_video/_pit?keep_alive=1m'
response = requests.post(url)
json_data = json.loads(response.text)
pit_id = json_data['id']
# query
data = {
"size": 500, "query": {"match_all": {}},
"pit": {"id": pit_id, "keep_alive": "1m"},
"sort": [{"published": {"order": "desc"}}]
}
query_str = json.dumps(data)
url = self.ES_URL + '/_search'
all_indexed = []
while True:
response = requests.get(url, data=query_str, headers=headers)
json_data = json.loads(response.text)
all_hits = json_data['hits']['hits']
if all_hits:
for hit in all_hits:
all_indexed.append(hit)
search_after = hit['sort']
# update search_after with last hit data
data['search_after'] = search_after
query_str = json.dumps(data)
else:
break
# clean up PIT
query_str = json.dumps({"id": pit_id})
requests.delete(self.ES_URL + '/_pit', data=query_str, headers=headers)
return all_indexed
def get_all_downloaded(self):
""" get a list of all videos in archive """
all_channel_folders = os.listdir(self.VIDEOS)
all_downloaded = []
for channel_folder in all_channel_folders:
channel_path = os.path.join(self.VIDEOS, channel_folder)
all_videos = os.listdir(channel_path)
youtube_vids = [i[9:20] for i in all_videos]
for youtube_id in youtube_vids:
all_downloaded.append(youtube_id)
return all_downloaded
def delete_from_pending(self, youtube_id):
""" delete the youtube_id from ta_download """
url = f'{self.ES_URL}/ta_download/_doc/{youtube_id}'
response = requests.delete(url)
if not response.ok:
print(response.text)
def ignore_from_pending(self, ignore_list):
""" build the bulk query string """
stamp = int(datetime.now().strftime("%s"))
bulk_list = []
for youtube_id in ignore_list:
action = {"update": {"_id": youtube_id, "_index": "ta_download"}}
source = {"doc": {"status": 'ignore', "timestamp": stamp}}
bulk_list.append(json.dumps(action))
bulk_list.append(json.dumps(source))
# add last newline
bulk_list.append('\n')
query_str = '\n'.join(bulk_list)
headers = {'Content-type': 'application/x-ndjson'}
url = self.ES_URL + '/_bulk'
request = requests.post(url, data=query_str, headers=headers)
mess_dict = {
"status": "ignore",
"level": "info",
"title": "Added to ignore list",
"message": ''
}
set_message('progress:download', mess_dict)
if not request.ok:
print(request)
class ChannelSubscription:
""" manage the list of channels subscribed """
def __init__(self):
config = AppConfig().config
self.es_url = config['application']['es_url']
self.channel_size = config['subscriptions']['channel_size']
def get_channels(self, subscribed_only=True):
""" get a list of all channels subscribed to """
headers = {'Content-type': 'application/json'}
# get PIT ID
url = self.es_url + '/ta_channel/_pit?keep_alive=1m'
response = requests.post(url)
json_data = json.loads(response.text)
pit_id = json_data['id']
# query
if subscribed_only:
data = {
"query": {"term": {"channel_subscribed": {"value": True}}},
"size": 50, "pit": {"id": pit_id, "keep_alive": "1m"},
"sort": [{"channel_name.keyword": {"order": "asc"}}]
}
else:
data = {
"query": {"match_all": {}},
"size": 50, "pit": {"id": pit_id, "keep_alive": "1m"},
"sort": [{"channel_name.keyword": {"order": "asc"}}]
}
query_str = json.dumps(data)
url = self.es_url + '/_search'
all_channels = []
while True:
response = requests.get(url, data=query_str, headers=headers)
json_data = json.loads(response.text)
all_hits = json_data['hits']['hits']
if all_hits:
for hit in all_hits:
source = hit['_source']
search_after = hit['sort']
all_channels.append(source)
# update search_after with last hit data
data['search_after'] = search_after
query_str = json.dumps(data)
else:
break
# clean up PIT
query_str = json.dumps({"id": pit_id})
requests.delete(self.es_url + '/_pit', data=query_str, headers=headers)
return all_channels
def get_last_youtube_videos(self, channel_id, limit=True):
""" get a list of last videos from channel """
url = f'https://www.youtube.com/channel/{channel_id}/videos'
obs = {
'default_search': 'ytsearch', 'quiet': True,
'skip_download': True, 'extract_flat': True
}
if limit:
obs['playlistend'] = self.channel_size
chan = youtube_dl.YoutubeDL(obs).extract_info(url, download=False)
last_videos = [(i['id'], i['title']) for i in chan['entries']]
return last_videos
def find_missing(self):
""" add missing videos from subscribed channels to pending """
all_channels = self.get_channels()
pending_handler = PendingList()
all_pending, all_ignore = pending_handler.get_all_pending()
all_pending_ids = [i['youtube_id'] for i in all_pending]
all_downloaded = pending_handler.get_all_downloaded()
to_ignore = all_pending_ids + all_ignore + all_downloaded
missing_videos = []
counter = 1
for channel in all_channels:
channel_id = channel['channel_id']
last_videos = self.get_last_youtube_videos(channel_id)
set_message('progress:download', {
"status": "rescan",
"level": "info",
"title": "Rescanning: Looking for new videos.",
"message": f'Progress: {counter}/{len(all_channels)}'
}
)
for video in last_videos:
youtube_id = video[0]
if youtube_id not in to_ignore:
missing_videos.append(youtube_id)
counter = counter + 1
return missing_videos
def change_subscribe(self, channel_id, channel_subscribed):
""" subscribe or unsubscribe from channel and update """
if not isinstance(channel_subscribed, bool):
print('invalid status, should be bool')
return
headers = {'Content-type': 'application/json'}
channel_handler = YoutubeChannel(channel_id)
channel_dict = channel_handler.channel_dict
channel_dict['channel_subscribed'] = channel_subscribed
if channel_subscribed:
# handle subscribe
url = self.es_url + '/ta_channel/_doc/' + channel_id
payload = json.dumps(channel_dict)
print(channel_dict)
else:
url = self.es_url + '/ta_channel/_update/' + channel_id
payload = json.dumps({'doc': channel_dict})
# update channel
request = requests.post(url, data=payload, headers=headers)
if not request.ok:
print(request.text)
# sync to videos
channel_handler.sync_to_videos()
def playlist_extractor(playlist_id):
""" return youtube_ids from a playlist_id """
url = 'https://www.youtube.com/playlist?list=' + playlist_id
obs = {
'default_search': 'ytsearch', 'quiet': True, 'ignoreerrors': True,
'skip_download': True, 'extract_flat': True
}
playlist = youtube_dl.YoutubeDL(obs).extract_info(url, download=False)
playlist_vids = [(i['id'], i['title']) for i in playlist['entries']]
return playlist_vids
class VideoDownloader:
""" handle the video download functionality """
def __init__(self, youtube_id_list):
self.youtube_id_list = youtube_id_list
self.config = AppConfig().config
def download_list(self):
""" download the list of youtube_ids """
limit_count = self.config['downloads']['limit_count']
if limit_count:
self.youtube_id_list = self.youtube_id_list[:limit_count]
for youtube_id in self.youtube_id_list:
try:
self.dl_single_vid(youtube_id)
except youtube_dl.utils.DownloadError:
print('failed to download ' + youtube_id)
continue
vid_dict = index_new_video(youtube_id)
self.move_to_archive(vid_dict)
self.delete_from_pending(youtube_id)
if self.config['downloads']['sleep_interval']:
sleep(self.config['downloads']['sleep_interval'])
@staticmethod
def progress_hook(response):
""" process the progress_hooks from youtube_dl """
# title
filename = response['filename'][12:].replace('_', ' ')
title = "Downloading: " + os.path.split(filename)[-1]
# message
try:
percent = response['_percent_str']
size = response['_total_bytes_str']
speed = response['_speed_str']
eta = response['_eta_str']
message = f'{percent} of {size} at {speed} - time left: {eta}'
except KeyError:
message = ''
mess_dict = {
"status": "downloading",
"level": "info",
"title": title,
"message": message
}
set_message('progress:download', mess_dict)
def dl_single_vid(self, youtube_id):
""" download single video """
obs = {
'default_search': 'ytsearch',
'merge_output_format': 'mp4', 'restrictfilenames': True,
'outtmpl': (self.config['application']['cache_dir'] +
'/download/' +
self.config['application']['file_template']),
'progress_hooks': [self.progress_hook],
'quiet': True, 'continuedl': True, 'retries': 3
}
if self.config['downloads']['format']:
obs['format'] = self.config['downloads']['format']
if self.config['downloads']['limit_speed']:
obs['ratelimit'] = self.config['downloads']['limit_speed'] * 1024
external = False
if external:
obs['external_downloader'] = 'aria2c'
# check if already in cache to continue from there
cache_dir = self.config['application']['cache_dir']
all_cached = os.listdir(cache_dir + '/download/')
for file_name in all_cached:
if youtube_id in file_name:
obs['outtmpl'] = cache_dir + '/download/' + file_name
with youtube_dl.YoutubeDL(obs) as ydl:
try:
ydl.download([youtube_id])
except youtube_dl.utils.DownloadError:
print('retry failed download: ' + youtube_id)
sleep(10)
ydl.download([youtube_id])
def move_to_archive(self, vid_dict):
""" move downloaded video from cache to archive """
videos = self.config['application']['videos']
channel_name = vid_dict['channel']['channel_name']
channel_name_clean = clean_string(channel_name)
media_url = vid_dict['media_url']
youtube_id = vid_dict['youtube_id']
# make archive folder
videos = self.config['application']['videos']
new_folder = os.path.join(videos, channel_name_clean)
os.makedirs(new_folder, exist_ok=True)
# find real filename
cache_dir = self.config['application']['cache_dir']
for file_str in os.listdir(cache_dir + '/download'):
if youtube_id in file_str:
old_file = file_str
old_file_path = os.path.join(cache_dir, 'download', old_file)
new_file_path = os.path.join(videos, media_url)
# move and fix permission
shutil.move(old_file_path, new_file_path)
os.chown(
new_file_path,
self.config['application']['HOST_UID'],
self.config['application']['HOST_GID']
)
def delete_from_pending(self, youtube_id):
""" delete downloaded video from pending index if its there """
es_url = self.config['application']['es_url']
url = f'{es_url}/ta_download/_doc/{youtube_id}'
response = requests.delete(url)
if not response.ok and not response.status_code == 404:
print(response.text)

View File

@@ -0,0 +1,340 @@
"""
Functionality:
- handle download queue
- linked with ta_dowload index
"""
import json
from datetime import datetime
from home.src.download.subscriptions import (
ChannelSubscription,
PlaylistSubscription,
)
from home.src.download.thumbnails import ThumbManager
from home.src.download.yt_dlp_base import YtWrap
from home.src.es.connect import ElasticWrap, IndexPaginate
from home.src.index.playlist import YoutubePlaylist
from home.src.index.video_constants import VideoTypeEnum
from home.src.ta.config import AppConfig
from home.src.ta.helper import DurationConverter, is_shorts
from home.src.ta.ta_redis import RedisArchivist
class PendingIndex:
"""base class holding all export methods"""
def __init__(self):
self.all_pending = False
self.all_ignored = False
self.all_videos = False
self.all_channels = False
self.channel_overwrites = False
self.video_overwrites = False
self.to_skip = False
def get_download(self):
"""get a list of all pending videos in ta_download"""
data = {
"query": {"match_all": {}},
"sort": [{"timestamp": {"order": "asc"}}],
}
all_results = IndexPaginate("ta_download", data).get_results()
self.all_pending = []
self.all_ignored = []
self.to_skip = []
for result in all_results:
self.to_skip.append(result["youtube_id"])
if result["status"] == "pending":
self.all_pending.append(result)
elif result["status"] == "ignore":
self.all_ignored.append(result)
def get_indexed(self):
"""get a list of all videos indexed"""
data = {
"query": {"match_all": {}},
"sort": [{"published": {"order": "desc"}}],
}
self.all_videos = IndexPaginate("ta_video", data).get_results()
for video in self.all_videos:
self.to_skip.append(video["youtube_id"])
def get_channels(self):
"""get a list of all channels indexed"""
self.all_channels = []
self.channel_overwrites = {}
data = {
"query": {"match_all": {}},
"sort": [{"channel_id": {"order": "asc"}}],
}
channels = IndexPaginate("ta_channel", data).get_results()
for channel in channels:
channel_id = channel["channel_id"]
self.all_channels.append(channel_id)
if channel.get("channel_overwrites"):
self.channel_overwrites.update(
{channel_id: channel.get("channel_overwrites")}
)
self._map_overwrites()
def _map_overwrites(self):
"""map video ids to channel ids overwrites"""
self.video_overwrites = {}
for video in self.all_pending:
video_id = video["youtube_id"]
channel_id = video["channel_id"]
overwrites = self.channel_overwrites.get(channel_id, False)
if overwrites:
self.video_overwrites.update({video_id: overwrites})
class PendingInteract:
"""interact with items in download queue"""
def __init__(self, youtube_id=False, status=False):
self.youtube_id = youtube_id
self.status = status
def delete_item(self):
"""delete single item from pending"""
path = f"ta_download/_doc/{self.youtube_id}"
_, _ = ElasticWrap(path).delete(refresh=True)
def delete_by_status(self):
"""delete all matching item by status"""
data = {"query": {"term": {"status": {"value": self.status}}}}
path = "ta_download/_delete_by_query"
_, _ = ElasticWrap(path).post(data=data)
def update_status(self):
"""update status field of pending item"""
data = {"doc": {"status": self.status}}
path = f"ta_download/_update/{self.youtube_id}"
_, _ = ElasticWrap(path).post(data=data)
def get_item(self):
"""return pending item dict"""
path = f"ta_download/_doc/{self.youtube_id}"
response, status_code = ElasticWrap(path).get()
return response["_source"], status_code
def get_channel(self):
"""
get channel metadata from queue to not depend on channel to be indexed
"""
data = {
"size": 1,
"query": {"term": {"channel_id": {"value": self.youtube_id}}},
}
response, _ = ElasticWrap("ta_download/_search").get(data=data)
hits = response["hits"]["hits"]
if not hits:
channel_name = "NA"
else:
channel_name = hits[0]["_source"].get("channel_name", "NA")
return {
"channel_id": self.youtube_id,
"channel_name": channel_name,
}
class PendingList(PendingIndex):
"""manage the pending videos list"""
yt_obs = {
"default_search": "ytsearch",
"quiet": True,
"check_formats": "selected",
"noplaylist": True,
"writethumbnail": True,
"simulate": True,
"socket_timeout": 3,
}
def __init__(self, youtube_ids=False):
super().__init__()
self.config = AppConfig().config
self.youtube_ids = youtube_ids
self.to_skip = False
self.missing_videos = False
def parse_url_list(self):
"""extract youtube ids from list"""
self.missing_videos = []
self.get_download()
self.get_indexed()
for entry in self.youtube_ids:
# notify
mess_dict = {
"status": "message:add",
"level": "info",
"title": "Adding to download queue.",
"message": "Extracting lists",
}
RedisArchivist().set_message("message:add", mess_dict, expire=True)
self._process_entry(entry)
def _process_entry(self, entry):
"""process single entry from url list"""
vid_type = self._get_vid_type(entry)
if entry["type"] == "video":
self._add_video(entry["url"], vid_type)
elif entry["type"] == "channel":
self._parse_channel(entry["url"], vid_type)
elif entry["type"] == "playlist":
self._parse_playlist(entry["url"])
PlaylistSubscription().process_url_str([entry], subscribed=False)
else:
raise ValueError(f"invalid url_type: {entry}")
@staticmethod
def _get_vid_type(entry):
"""add vid type enum if available"""
vid_type_str = entry.get("vid_type")
if not vid_type_str:
return VideoTypeEnum.UNKNOWN
return VideoTypeEnum(vid_type_str)
def _add_video(self, url, vid_type):
"""add video to list"""
if url not in self.missing_videos and url not in self.to_skip:
self.missing_videos.append((url, vid_type))
else:
print(f"{url}: skipped adding already indexed video to download.")
def _parse_channel(self, url, vid_type):
"""add all videos of channel to list"""
video_results = ChannelSubscription().get_last_youtube_videos(
url, limit=False, query_filter=vid_type
)
for video_id, _, vid_type in video_results:
self._add_video(video_id, vid_type)
def _parse_playlist(self, url):
"""add all videos of playlist to list"""
playlist = YoutubePlaylist(url)
playlist.build_json()
video_results = playlist.json_data.get("playlist_entries")
youtube_ids = [i["youtube_id"] for i in video_results]
for video_id in youtube_ids:
# match vid_type later
self._add_video(video_id, VideoTypeEnum.UNKNOWN)
def add_to_pending(self, status="pending"):
"""add missing videos to pending list"""
self.get_channels()
bulk_list = []
for idx, (youtube_id, vid_type) in enumerate(self.missing_videos):
print(f"{youtube_id} ({vid_type}): add to download queue")
video_details = self.get_youtube_details(youtube_id, vid_type)
if not video_details:
continue
video_details["status"] = status
action = {"create": {"_id": youtube_id, "_index": "ta_download"}}
bulk_list.append(json.dumps(action))
bulk_list.append(json.dumps(video_details))
url = video_details["vid_thumb_url"]
ThumbManager(youtube_id).download_video_thumb(url)
self._notify_add(idx)
if bulk_list:
# add last newline
bulk_list.append("\n")
query_str = "\n".join(bulk_list)
_, _ = ElasticWrap("_bulk").post(query_str, ndjson=True)
def _notify_add(self, idx):
"""send notification for adding videos to download queue"""
progress = f"{idx + 1}/{len(self.missing_videos)}"
mess_dict = {
"status": "message:add",
"level": "info",
"title": "Adding new videos to download queue.",
"message": "Progress: " + progress,
}
if idx + 1 == len(self.missing_videos):
expire = 4
else:
expire = True
RedisArchivist().set_message("message:add", mess_dict, expire=expire)
if idx + 1 % 25 == 0:
print("adding to queue progress: " + progress)
def get_youtube_details(self, youtube_id, vid_type=VideoTypeEnum.VIDEOS):
"""get details from youtubedl for single pending video"""
vid = YtWrap(self.yt_obs, self.config).extract(youtube_id)
if not vid:
return False
if vid.get("id") != youtube_id:
# skip premium videos with different id
print(f"{youtube_id}: skipping premium video, id not matching")
return False
# stop if video is streaming live now
if vid["live_status"] in ["is_upcoming", "is_live"]:
print(f"{youtube_id}: skip is_upcoming or is_live")
return False
if vid["live_status"] == "was_live":
vid_type = VideoTypeEnum.STREAMS
else:
if self._check_shorts(vid):
vid_type = VideoTypeEnum.SHORTS
else:
vid_type = VideoTypeEnum.VIDEOS
return self._parse_youtube_details(vid, vid_type)
@staticmethod
def _check_shorts(vid):
"""check if vid is shorts video"""
if vid["width"] > vid["height"]:
return False
duration = vid.get("duration")
if duration and isinstance(duration, int):
if duration > 60:
return False
return is_shorts(vid["id"])
def _parse_youtube_details(self, vid, vid_type=VideoTypeEnum.VIDEOS):
"""parse response"""
vid_id = vid.get("id")
duration_str = DurationConverter.get_str(vid["duration"])
if duration_str == "NA":
print(f"skip extracting duration for: {vid_id}")
published = datetime.strptime(vid["upload_date"], "%Y%m%d").strftime(
"%Y-%m-%d"
)
# build dict
youtube_details = {
"youtube_id": vid_id,
"channel_name": vid["channel"],
"vid_thumb_url": vid["thumbnail"],
"title": vid["title"],
"channel_id": vid["channel_id"],
"duration": duration_str,
"published": published,
"timestamp": int(datetime.now().timestamp()),
# Pulling enum value out so it is serializable
"vid_type": vid_type.value,
}
if self.all_channels:
youtube_details.update(
{"channel_indexed": vid["channel_id"] in self.all_channels}
)
return youtube_details

View File

@@ -0,0 +1,267 @@
"""
Functionality:
- handle channel subscriptions
- handle playlist subscriptions
"""
from home.src.download import queue # partial import
from home.src.download.thumbnails import ThumbManager
from home.src.download.yt_dlp_base import YtWrap
from home.src.es.connect import IndexPaginate
from home.src.index.channel import YoutubeChannel
from home.src.index.playlist import YoutubePlaylist
from home.src.index.video_constants import VideoTypeEnum
from home.src.ta.config import AppConfig
from home.src.ta.ta_redis import RedisArchivist
class ChannelSubscription:
"""manage the list of channels subscribed"""
def __init__(self):
self.config = AppConfig().config
@staticmethod
def get_channels(subscribed_only=True):
"""get a list of all channels subscribed to"""
data = {
"sort": [{"channel_name.keyword": {"order": "asc"}}],
}
if subscribed_only:
data["query"] = {"term": {"channel_subscribed": {"value": True}}}
else:
data["query"] = {"match_all": {}}
all_channels = IndexPaginate("ta_channel", data).get_results()
return all_channels
def get_last_youtube_videos(
self, channel_id, limit=True, query_filter=VideoTypeEnum.UNKNOWN
):
"""get a list of last videos from channel"""
queries = self._build_queries(query_filter, limit)
last_videos = []
for vid_type, limit_amount in queries:
obs = {
"skip_download": True,
"extract_flat": True,
}
if limit:
obs["playlistend"] = limit_amount
path = vid_type.value
channel = YtWrap(obs, self.config).extract(
f"https://www.youtube.com/channel/{channel_id}/{path}"
)
if not channel:
continue
last_videos.extend(
[(i["id"], i["title"], vid_type) for i in channel["entries"]]
)
return last_videos
def _build_queries(self, query_filter, limit):
"""build query list for vid_type"""
limit_map = {
"videos": self.config["subscriptions"]["channel_size"],
"streams": self.config["subscriptions"]["live_channel_size"],
"shorts": self.config["subscriptions"]["shorts_channel_size"],
}
queries = []
if query_filter and query_filter.value != "unknown":
if limit:
query_limit = limit_map.get(query_filter.value)
else:
query_limit = False
queries.append((query_filter, query_limit))
return queries
for query_item, default_limit in limit_map.items():
if not default_limit:
# is deactivated in config
continue
if limit:
query_limit = default_limit
else:
query_limit = False
queries.append((VideoTypeEnum(query_item), query_limit))
return queries
def find_missing(self):
"""add missing videos from subscribed channels to pending"""
all_channels = self.get_channels()
pending = queue.PendingList()
pending.get_download()
pending.get_indexed()
missing_videos = []
for idx, channel in enumerate(all_channels):
channel_id = channel["channel_id"]
print(f"{channel_id}: find missing videos.")
last_videos = self.get_last_youtube_videos(channel_id)
if last_videos:
for video_id, _, vid_type in last_videos:
if video_id not in pending.to_skip:
missing_videos.append((video_id, vid_type))
# notify
message = {
"status": "message:rescan",
"level": "info",
"title": "Scanning channels: Looking for new videos.",
"message": f"Progress: {idx + 1}/{len(all_channels)}",
}
if idx + 1 == len(all_channels):
expire = 4
else:
expire = True
RedisArchivist().set_message(
"message:rescan", message=message, expire=expire
)
return missing_videos
@staticmethod
def change_subscribe(channel_id, channel_subscribed):
"""subscribe or unsubscribe from channel and update"""
channel = YoutubeChannel(channel_id)
channel.build_json()
channel.json_data["channel_subscribed"] = channel_subscribed
channel.upload_to_es()
channel.sync_to_videos()
class PlaylistSubscription:
"""manage the playlist download functionality"""
def __init__(self):
self.config = AppConfig().config
@staticmethod
def get_playlists(subscribed_only=True):
"""get a list of all active playlists"""
data = {
"sort": [{"playlist_channel.keyword": {"order": "desc"}}],
}
data["query"] = {
"bool": {"must": [{"term": {"playlist_active": {"value": True}}}]}
}
if subscribed_only:
data["query"]["bool"]["must"].append(
{"term": {"playlist_subscribed": {"value": True}}}
)
all_playlists = IndexPaginate("ta_playlist", data).get_results()
return all_playlists
def process_url_str(self, new_playlists, subscribed=True):
"""process playlist subscribe form url_str"""
data = {
"query": {"match_all": {}},
"sort": [{"published": {"order": "desc"}}],
}
all_indexed = IndexPaginate("ta_video", data).get_results()
all_youtube_ids = [i["youtube_id"] for i in all_indexed]
for idx, playlist in enumerate(new_playlists):
playlist_id = playlist["url"]
if not playlist["type"] == "playlist":
print(f"{playlist_id} not a playlist, skipping...")
continue
playlist_h = YoutubePlaylist(playlist_id)
playlist_h.all_youtube_ids = all_youtube_ids
playlist_h.build_json()
playlist_h.json_data["playlist_subscribed"] = subscribed
playlist_h.upload_to_es()
playlist_h.add_vids_to_playlist()
self.channel_validate(playlist_h.json_data["playlist_channel_id"])
url = playlist_h.json_data["playlist_thumbnail"]
thumb = ThumbManager(playlist_id, item_type="playlist")
thumb.download_playlist_thumb(url)
# notify
message = {
"status": "message:subplaylist",
"level": "info",
"title": "Subscribing to Playlists",
"message": f"Processing {idx + 1} of {len(new_playlists)}",
}
RedisArchivist().set_message(
"message:subplaylist", message=message, expire=True
)
@staticmethod
def channel_validate(channel_id):
"""make sure channel of playlist is there"""
channel = YoutubeChannel(channel_id)
channel.build_json(upload=True)
@staticmethod
def change_subscribe(playlist_id, subscribe_status):
"""change the subscribe status of a playlist"""
playlist = YoutubePlaylist(playlist_id)
playlist.build_json()
playlist.json_data["playlist_subscribed"] = subscribe_status
playlist.upload_to_es()
@staticmethod
def get_to_ignore():
"""get all youtube_ids already downloaded or ignored"""
pending = queue.PendingList()
pending.get_download()
pending.get_indexed()
return pending.to_skip
def find_missing(self):
"""find videos in subscribed playlists not downloaded yet"""
all_playlists = [i["playlist_id"] for i in self.get_playlists()]
to_ignore = self.get_to_ignore()
missing_videos = []
for idx, playlist_id in enumerate(all_playlists):
size_limit = self.config["subscriptions"]["channel_size"]
playlist = YoutubePlaylist(playlist_id)
playlist.update_playlist()
if not playlist:
playlist.deactivate()
continue
playlist_entries = playlist.json_data["playlist_entries"]
if size_limit:
del playlist_entries[size_limit:]
all_missing = [i for i in playlist_entries if not i["downloaded"]]
message = {
"status": "message:rescan",
"level": "info",
"title": "Scanning playlists: Looking for new videos.",
"message": f"Progress: {idx + 1}/{len(all_playlists)}",
}
RedisArchivist().set_message(
"message:rescan", message=message, expire=True
)
for video in all_missing:
youtube_id = video["youtube_id"]
if youtube_id not in to_ignore:
missing_videos.append(youtube_id)
return missing_videos

View File

@@ -0,0 +1,373 @@
"""
functionality:
- handle download and caching for thumbnails
- check for missing thumbnails
"""
import base64
import os
from io import BytesIO
from time import sleep
import requests
from home.src.download import queue # partial import
from home.src.es.connect import IndexPaginate
from home.src.ta.config import AppConfig
from mutagen.mp4 import MP4, MP4Cover
from PIL import Image, ImageFile, ImageFilter, UnidentifiedImageError
ImageFile.LOAD_TRUNCATED_IMAGES = True
class ThumbManagerBase:
"""base class for thumbnail management"""
CONFIG = AppConfig().config
CACHE_DIR = CONFIG["application"]["cache_dir"]
VIDEO_DIR = os.path.join(CACHE_DIR, "videos")
CHANNEL_DIR = os.path.join(CACHE_DIR, "channels")
PLAYLIST_DIR = os.path.join(CACHE_DIR, "playlists")
def __init__(self, item_id, item_type, fallback=False):
self.item_id = item_id
self.item_type = item_type
self.fallback = fallback
def download_raw(self, url):
"""download thumbnail for video"""
if not url:
return self.get_fallback()
for i in range(3):
try:
response = requests.get(url, stream=True, timeout=5)
if response.ok:
try:
img = Image.open(response.raw)
if isinstance(img, Image.Image):
return img
return self.get_fallback()
except (UnidentifiedImageError, OSError):
print(f"failed to open thumbnail: {url}")
return self.get_fallback()
if response.status_code == 404:
return self.get_fallback()
except requests.exceptions.RequestException:
print(f"{self.item_id}: retry thumbnail download {url}")
sleep((i + 1) ** i)
return False
def get_fallback(self):
"""get fallback thumbnail if not available"""
print(f"{self.item_id}: failed to extract thumbnail, use fallback")
if self.fallback:
img_raw = Image.open(self.fallback)
return img_raw
app_root = self.CONFIG["application"]["app_root"]
default_map = {
"video": os.path.join(
app_root, "static/img/default-video-thumb.jpg"
),
"playlist": os.path.join(
app_root, "static/img/default-video-thumb.jpg"
),
"icon": os.path.join(
app_root, "static/img/default-channel-icon.jpg"
),
"banner": os.path.join(
app_root, "static/img/default-channel-banner.jpg"
),
}
img_raw = Image.open(default_map[self.item_type])
return img_raw
class ThumbManager(ThumbManagerBase):
"""handle thumbnails related functions"""
def __init__(self, item_id, item_type="video", fallback=False):
super().__init__(item_id, item_type, fallback=fallback)
def download(self, url):
"""download thumbnail"""
print(f"{self.item_id}: download {self.item_type} thumbnail")
if self.item_type == "video":
self.download_video_thumb(url)
elif self.item_type == "channel":
self.download_channel_art(url)
elif self.item_type == "playlist":
self.download_playlist_thumb(url)
def delete(self):
"""delete thumbnail file"""
print(f"{self.item_id}: delete {self.item_type} thumbnail")
if self.item_type == "video":
self.delete_video_thumb()
elif self.item_type == "channel":
self.delete_channel_thumb()
elif self.item_type == "playlist":
self.delete_playlist_thumb()
def download_video_thumb(self, url, skip_existing=False):
"""pass url for video thumbnail"""
folder_path = os.path.join(self.VIDEO_DIR, self.item_id[0].lower())
thumb_path = self.vid_thumb_path(absolute=True)
if skip_existing and os.path.exists(thumb_path):
return
os.makedirs(folder_path, exist_ok=True)
img_raw = self.download_raw(url)
width, height = img_raw.size
if not width / height == 16 / 9:
new_height = width / 16 * 9
offset = (height - new_height) / 2
img_raw = img_raw.crop((0, offset, width, height - offset))
img_raw.convert("RGB").save(thumb_path)
def vid_thumb_path(self, absolute=False, create_folder=False):
"""build expected path for video thumbnail from youtube_id"""
folder_name = self.item_id[0].lower()
folder_path = os.path.join("videos", folder_name)
thumb_path = os.path.join(folder_path, f"{self.item_id}.jpg")
if absolute:
thumb_path = os.path.join(self.CACHE_DIR, thumb_path)
if create_folder:
folder_path = os.path.join(self.CACHE_DIR, folder_path)
os.makedirs(folder_path, exist_ok=True)
return thumb_path
def download_channel_art(self, urls, skip_existing=False):
"""pass tuple of channel thumbnails"""
channel_thumb, channel_banner = urls
self._download_channel_thumb(channel_thumb, skip_existing)
self._download_channel_banner(channel_banner, skip_existing)
def _download_channel_thumb(self, channel_thumb, skip_existing):
"""download channel thumbnail"""
thumb_path = os.path.join(
self.CHANNEL_DIR, f"{self.item_id}_thumb.jpg"
)
self.item_type = "icon"
if skip_existing and os.path.exists(thumb_path):
return
img_raw = self.download_raw(channel_thumb)
img_raw.convert("RGB").save(thumb_path)
def _download_channel_banner(self, channel_banner, skip_existing):
"""download channel banner"""
banner_path = os.path.join(
self.CHANNEL_DIR, self.item_id + "_banner.jpg"
)
self.item_type = "banner"
if skip_existing and os.path.exists(banner_path):
return
img_raw = self.download_raw(channel_banner)
img_raw.convert("RGB").save(banner_path)
def download_playlist_thumb(self, url, skip_existing=False):
"""pass thumbnail url"""
thumb_path = os.path.join(self.PLAYLIST_DIR, f"{self.item_id}.jpg")
if skip_existing and os.path.exists(thumb_path):
return
img_raw = self.download_raw(url)
img_raw.convert("RGB").save(thumb_path)
def delete_video_thumb(self):
"""delete video thumbnail if exists"""
thumb_path = self.vid_thumb_path()
to_delete = os.path.join(self.CACHE_DIR, thumb_path)
if os.path.exists(to_delete):
os.remove(to_delete)
def delete_channel_thumb(self):
"""delete all artwork of channel"""
thumb = os.path.join(self.CHANNEL_DIR, f"{self.item_id}_thumb.jpg")
banner = os.path.join(self.CHANNEL_DIR, f"{self.item_id}_banner.jpg")
if os.path.exists(thumb):
os.remove(thumb)
if os.path.exists(banner):
os.remove(banner)
def delete_playlist_thumb(self):
"""delete playlist thumbnail"""
thumb_path = os.path.join(self.PLAYLIST_DIR, f"{self.item_id}.jpg")
if os.path.exists(thumb_path):
os.remove(thumb_path)
def get_vid_base64_blur(self):
"""return base64 encoded placeholder"""
file_path = os.path.join(self.CACHE_DIR, self.vid_thumb_path())
img_raw = Image.open(file_path)
img_raw.thumbnail((img_raw.width // 20, img_raw.height // 20))
img_blur = img_raw.filter(ImageFilter.BLUR)
buffer = BytesIO()
img_blur.save(buffer, format="JPEG")
img_data = buffer.getvalue()
img_base64 = base64.b64encode(img_data).decode()
data_url = f"data:image/jpg;base64,{img_base64}"
return data_url
class ValidatorCallback:
"""handle callback validate thumbnails page by page"""
def __init__(self, source, index_name):
self.source = source
self.index_name = index_name
def run(self):
"""run the task for page"""
print(f"{self.index_name}: validate artwork")
if self.index_name == "ta_video":
self._validate_videos()
elif self.index_name == "ta_channel":
self._validate_channels()
elif self.index_name == "ta_playlist":
self._validate_playlists()
def _validate_videos(self):
"""check if video thumbnails are correct"""
for video in self.source:
url = video["_source"]["vid_thumb_url"]
handler = ThumbManager(video["_source"]["youtube_id"])
handler.download_video_thumb(url, skip_existing=True)
def _validate_channels(self):
"""check if all channel artwork is there"""
for channel in self.source:
urls = (
channel["_source"]["channel_thumb_url"],
channel["_source"]["channel_banner_url"],
)
handler = ThumbManager(channel["_source"]["channel_id"])
handler.download_channel_art(urls, skip_existing=True)
def _validate_playlists(self):
"""check if all playlist artwork is there"""
for playlist in self.source:
url = playlist["_source"]["playlist_thumbnail"]
handler = ThumbManager(playlist["_source"]["playlist_id"])
handler.download_playlist_thumb(url, skip_existing=True)
class ThumbValidator:
"""validate thumbnails"""
def download_missing(self):
"""download all missing artwork"""
self.download_missing_videos()
self.download_missing_channels()
self.download_missing_playlists()
def download_missing_videos(self):
"""get all missing video thumbnails"""
data = {
"query": {"term": {"active": {"value": True}}},
"sort": [{"youtube_id": {"order": "asc"}}],
"_source": ["vid_thumb_url", "youtube_id"],
}
paginate = IndexPaginate(
"ta_video", data, size=5000, callback=ValidatorCallback
)
_ = paginate.get_results()
def download_missing_channels(self):
"""get all missing channel thumbnails"""
data = {
"query": {"term": {"channel_active": {"value": True}}},
"sort": [{"channel_id": {"order": "asc"}}],
"_source": {
"excludes": ["channel_description", "channel_overwrites"]
},
}
paginate = IndexPaginate(
"ta_channel", data, callback=ValidatorCallback
)
_ = paginate.get_results()
def download_missing_playlists(self):
"""get all missing playlist artwork"""
data = {
"query": {"term": {"playlist_active": {"value": True}}},
"sort": [{"playlist_id": {"order": "asc"}}],
"_source": ["playlist_id", "playlist_thumbnail"],
}
paginate = IndexPaginate(
"ta_playlist", data, callback=ValidatorCallback
)
_ = paginate.get_results()
class ThumbFilesystem:
"""filesystem tasks for thumbnails"""
CONFIG = AppConfig().config
CACHE_DIR = CONFIG["application"]["cache_dir"]
MEDIA_DIR = CONFIG["application"]["videos"]
VIDEO_DIR = os.path.join(CACHE_DIR, "videos")
def sync(self):
"""embed thumbnails to mediafiles"""
video_list = self.get_thumb_list()
self._embed_thumbs(video_list)
def get_thumb_list(self):
"""get list of mediafiles and matching thumbnails"""
pending = queue.PendingList()
pending.get_download()
pending.get_indexed()
video_list = []
for video in pending.all_videos:
video_id = video["youtube_id"]
media_url = os.path.join(self.MEDIA_DIR, video["media_url"])
thumb_path = os.path.join(
self.CACHE_DIR, ThumbManager(video_id).vid_thumb_path()
)
video_list.append(
{
"media_url": media_url,
"thumb_path": thumb_path,
}
)
return video_list
@staticmethod
def _embed_thumbs(video_list):
"""rewrite the thumbnail into media file"""
counter = 1
for video in video_list:
# loop through all videos
media_url = video["media_url"]
thumb_path = video["thumb_path"]
mutagen_vid = MP4(media_url)
with open(thumb_path, "rb") as f:
mutagen_vid["covr"] = [
MP4Cover(f.read(), imageformat=MP4Cover.FORMAT_JPEG)
]
mutagen_vid.save()
if counter % 50 == 0:
print(f"thumbnail write progress {counter}/{len(video_list)}")
counter = counter + 1

View File

@@ -0,0 +1,157 @@
"""
functionality:
- base class to make all calls to yt-dlp
- handle yt-dlp errors
"""
import os
from datetime import datetime
from http import cookiejar
from io import StringIO
import yt_dlp
from home.src.ta.ta_redis import RedisArchivist
class YtWrap:
"""wrap calls to yt"""
OBS_BASE = {
"default_search": "ytsearch",
"quiet": True,
"check_formats": "selected",
"socket_timeout": 3,
"extractor_retries": 3,
}
def __init__(self, obs_request, config=False):
self.obs_request = obs_request
self.config = config
self.build_obs()
def build_obs(self):
"""build yt-dlp obs"""
self.obs = self.OBS_BASE.copy()
self.obs.update(self.obs_request)
if self.config:
self.add_cookie()
def add_cookie(self):
"""add cookie if enabled"""
if self.config["downloads"]["cookie_import"]:
cookie_io = CookieHandler(self.config).get()
self.obs["cookiefile"] = cookie_io
def download(self, url):
"""make download request"""
with yt_dlp.YoutubeDL(self.obs) as ydl:
try:
ydl.download([url])
except yt_dlp.utils.DownloadError:
print(f"{url}: failed to download.")
return False
return True
def extract(self, url):
"""make extract request"""
try:
response = yt_dlp.YoutubeDL(self.obs).extract_info(url)
except cookiejar.LoadError:
print("cookie file is invalid")
return False
except (yt_dlp.utils.ExtractorError, yt_dlp.utils.DownloadError):
print(f"{url}: failed to get info from youtube")
return False
return response
class CookieHandler:
"""handle youtube cookie for yt-dlp"""
def __init__(self, config):
self.cookie_io = False
self.config = config
def get(self):
"""get cookie io stream"""
cookie = RedisArchivist().get_message("cookie")
self.cookie_io = StringIO(cookie)
return self.cookie_io
def import_cookie(self):
"""import cookie from file"""
cache_path = self.config["application"]["cache_dir"]
import_path = os.path.join(cache_path, "import", "cookies.google.txt")
try:
with open(import_path, encoding="utf-8") as cookie_file:
cookie = cookie_file.read()
except FileNotFoundError as err:
print(f"cookie: {import_path} file not found")
raise err
self.set_cookie(cookie)
os.remove(import_path)
print("cookie: import successful")
def set_cookie(self, cookie):
"""set cookie str and activate in cofig"""
RedisArchivist().set_message("cookie", cookie)
path = ".downloads.cookie_import"
RedisArchivist().set_message("config", True, path=path)
self.config["downloads"]["cookie_import"] = True
print("cookie: activated and stored in Redis")
@staticmethod
def revoke():
"""revoke cookie"""
RedisArchivist().del_message("cookie")
RedisArchivist().del_message("cookie:valid")
RedisArchivist().set_message(
"config", False, path=".downloads.cookie_import"
)
print("cookie: revoked")
def validate(self):
"""validate cookie using the liked videos playlist"""
print("validating cookie")
obs_request = {
"skip_download": True,
"extract_flat": True,
}
validator = YtWrap(obs_request, self.config)
response = bool(validator.extract("LL"))
self.store_validation(response)
# update in redis to avoid expiring
modified = validator.obs["cookiefile"].getvalue()
if modified:
RedisArchivist().set_message("cookie", modified)
if not response:
mess_dict = {
"status": "message:download",
"level": "error",
"title": "Cookie validation failed, exiting...",
"message": "",
}
RedisArchivist().set_message(
"message:download", mess_dict, expire=4
)
print("cookie validation failed, exiting...")
return response
@staticmethod
def store_validation(response):
"""remember last validation"""
now = datetime.now()
message = {
"status": response,
"validated": int(now.timestamp()),
"validated_str": now.strftime("%Y-%m-%d %H:%M"),
}
RedisArchivist().set_message("cookie:valid", message)

View File

@@ -0,0 +1,465 @@
"""
functionality:
- handle yt_dlp
- build options and post processor
- download video files
- move to archive
"""
import json
import os
import shutil
from datetime import datetime
from home.src.download.queue import PendingList
from home.src.download.subscriptions import PlaylistSubscription
from home.src.download.yt_dlp_base import CookieHandler, YtWrap
from home.src.es.connect import ElasticWrap, IndexPaginate
from home.src.index.channel import YoutubeChannel
from home.src.index.comments import CommentList
from home.src.index.playlist import YoutubePlaylist
from home.src.index.video import YoutubeVideo, index_new_video
from home.src.index.video_constants import VideoTypeEnum
from home.src.ta.config import AppConfig
from home.src.ta.helper import clean_string, ignore_filelist
from home.src.ta.ta_redis import RedisArchivist, RedisQueue
class DownloadPostProcess:
"""handle task to run after download queue finishes"""
def __init__(self, download):
self.download = download
self.now = int(datetime.now().timestamp())
self.pending = False
def run(self):
"""run all functions"""
self.pending = PendingList()
self.pending.get_download()
self.pending.get_channels()
self.pending.get_indexed()
self.auto_delete_all()
self.auto_delete_overwrites()
self.validate_playlists()
self.get_comments()
def auto_delete_all(self):
"""handle auto delete"""
autodelete_days = self.download.config["downloads"]["autodelete_days"]
if not autodelete_days:
return
print(f"auto delete older than {autodelete_days} days")
now_lte = self.now - autodelete_days * 24 * 60 * 60
data = {
"query": {"range": {"player.watched_date": {"lte": now_lte}}},
"sort": [{"player.watched_date": {"order": "asc"}}],
}
self._auto_delete_watched(data)
def auto_delete_overwrites(self):
"""handle per channel auto delete from overwrites"""
for channel_id, value in self.pending.channel_overwrites.items():
if "autodelete_days" in value:
autodelete_days = value.get("autodelete_days")
print(f"{channel_id}: delete older than {autodelete_days}d")
now_lte = self.now - autodelete_days * 24 * 60 * 60
must_list = [
{"range": {"player.watched_date": {"lte": now_lte}}},
{"term": {"channel.channel_id": {"value": channel_id}}},
]
data = {
"query": {"bool": {"must": must_list}},
"sort": [{"player.watched_date": {"order": "desc"}}],
}
self._auto_delete_watched(data)
@staticmethod
def _auto_delete_watched(data):
"""delete watched videos after x days"""
to_delete = IndexPaginate("ta_video", data).get_results()
if not to_delete:
return
for video in to_delete:
youtube_id = video["youtube_id"]
print(f"{youtube_id}: auto delete video")
YoutubeVideo(youtube_id).delete_media_file()
print("add deleted to ignore list")
vids = [{"type": "video", "url": i["youtube_id"]} for i in to_delete]
pending = PendingList(youtube_ids=vids)
pending.parse_url_list()
pending.add_to_pending(status="ignore")
def validate_playlists(self):
"""look for playlist needing to update"""
for id_c, channel_id in enumerate(self.download.channels):
channel = YoutubeChannel(channel_id)
overwrites = self.pending.channel_overwrites.get(channel_id, False)
if overwrites and overwrites.get("index_playlists"):
# validate from remote
channel.index_channel_playlists()
continue
# validate from local
playlists = channel.get_indexed_playlists(active_only=True)
all_channel_playlist = [i["playlist_id"] for i in playlists]
self._validate_channel_playlist(all_channel_playlist, id_c)
def _validate_channel_playlist(self, all_channel_playlist, id_c):
"""scan channel for playlist needing update"""
all_youtube_ids = [i["youtube_id"] for i in self.pending.all_videos]
for id_p, playlist_id in enumerate(all_channel_playlist):
playlist = YoutubePlaylist(playlist_id)
playlist.all_youtube_ids = all_youtube_ids
playlist.build_json(scrape=True)
if not playlist.json_data:
playlist.deactivate()
continue
playlist.add_vids_to_playlist()
playlist.upload_to_es()
self._notify_playlist_progress(all_channel_playlist, id_c, id_p)
def _notify_playlist_progress(self, all_channel_playlist, id_c, id_p):
"""notify to UI"""
title = (
"Processing playlists for channels: "
+ f"{id_c + 1}/{len(self.download.channels)}"
)
message = f"Progress: {id_p + 1}/{len(all_channel_playlist)}"
key = "message:download"
mess_dict = {
"status": key,
"level": "info",
"title": title,
"message": message,
}
if id_p + 1 == len(all_channel_playlist):
expire = 4
else:
expire = True
RedisArchivist().set_message(key, mess_dict, expire=expire)
def get_comments(self):
"""get comments from youtube"""
CommentList(self.download.videos).index(notify=True)
class VideoDownloader:
"""
handle the video download functionality
if not initiated with list, take from queue
"""
MSG = "message:download"
def __init__(self, youtube_id_list=False):
self.obs = False
self.video_overwrites = False
self.youtube_id_list = youtube_id_list
self.config = AppConfig().config
self._build_obs()
self.channels = set()
self.videos = set()
def run_queue(self):
"""setup download queue in redis loop until no more items"""
self._setup_queue()
queue = RedisQueue(queue_name="dl_queue")
limit_queue = self.config["downloads"]["limit_count"]
if limit_queue:
queue.trim(limit_queue - 1)
while True:
youtube_data = queue.get_next()
if not youtube_data:
break
try:
youtube_data = json.loads(youtube_data)
except json.JSONDecodeError: # This many not be necessary
continue
youtube_id = youtube_data.get("youtube_id")
tmp_vid_type = youtube_data.get(
"vid_type", VideoTypeEnum.VIDEOS.value
)
video_type = VideoTypeEnum(tmp_vid_type)
print(f"Downloading type: {video_type}")
success = self._dl_single_vid(youtube_id)
if not success:
continue
mess_dict = {
"status": self.MSG,
"level": "info",
"title": "Indexing....",
"message": "Add video metadata to index.",
}
RedisArchivist().set_message(self.MSG, mess_dict, expire=60)
vid_dict = index_new_video(
youtube_id,
video_overwrites=self.video_overwrites,
video_type=video_type,
)
self.channels.add(vid_dict["channel"]["channel_id"])
self.videos.add(vid_dict["youtube_id"])
mess_dict = {
"status": self.MSG,
"level": "info",
"title": "Moving....",
"message": "Moving downloaded file to storage folder",
}
RedisArchivist().set_message(self.MSG, mess_dict)
if queue.has_item():
message = "Continue with next video."
else:
message = "Download queue is finished."
self.move_to_archive(vid_dict)
mess_dict = {
"status": self.MSG,
"level": "info",
"title": "Completed",
"message": message,
}
RedisArchivist().set_message(self.MSG, mess_dict, expire=10)
self._delete_from_pending(youtube_id)
# post processing
self._add_subscribed_channels()
DownloadPostProcess(self).run()
def _setup_queue(self):
"""setup required and validate"""
if self.config["downloads"]["cookie_import"]:
valid = CookieHandler(self.config).validate()
if not valid:
return
pending = PendingList()
pending.get_download()
pending.get_channels()
self.video_overwrites = pending.video_overwrites
def add_pending(self):
"""add pending videos to download queue"""
mess_dict = {
"status": self.MSG,
"level": "info",
"title": "Looking for videos to download",
"message": "Scanning your download queue.",
}
RedisArchivist().set_message(self.MSG, mess_dict, expire=True)
pending = PendingList()
pending.get_download()
to_add = [
json.dumps(
{
"youtube_id": i["youtube_id"],
# Using .value in default val to match what would be
# decoded when parsing json if not set
"vid_type": i.get("vid_type", VideoTypeEnum.VIDEOS.value),
}
)
for i in pending.all_pending
]
if not to_add:
# there is nothing pending
print("download queue is empty")
mess_dict = {
"status": self.MSG,
"level": "error",
"title": "Download queue is empty",
"message": "Add some videos to the queue first.",
}
RedisArchivist().set_message(self.MSG, mess_dict, expire=True)
return
RedisQueue(queue_name="dl_queue").add_list(to_add)
def _progress_hook(self, response):
"""process the progress_hooks from yt_dlp"""
title = "Downloading: " + response["info_dict"]["title"]
try:
percent = response["_percent_str"]
size = response["_total_bytes_str"]
speed = response["_speed_str"]
eta = response["_eta_str"]
message = f"{percent} of {size} at {speed} - time left: {eta}"
except KeyError:
message = "processing"
mess_dict = {
"status": self.MSG,
"level": "info",
"title": title,
"message": message,
}
RedisArchivist().set_message(self.MSG, mess_dict, expire=True)
def _build_obs(self):
"""collection to build all obs passed to yt-dlp"""
self._build_obs_basic()
self._build_obs_user()
self._build_obs_postprocessors()
def _build_obs_basic(self):
"""initial obs"""
self.obs = {
"default_search": "ytsearch",
"merge_output_format": "mp4",
"outtmpl": (
self.config["application"]["cache_dir"]
+ "/download/%(id)s.mp4"
),
"progress_hooks": [self._progress_hook],
"noprogress": True,
"quiet": True,
"continuedl": True,
"retries": 3,
"writethumbnail": False,
"noplaylist": True,
"check_formats": "selected",
"socket_timeout": 3,
}
def _build_obs_user(self):
"""build user customized options"""
if self.config["downloads"]["format"]:
self.obs["format"] = self.config["downloads"]["format"]
if self.config["downloads"]["limit_speed"]:
self.obs["ratelimit"] = (
self.config["downloads"]["limit_speed"] * 1024
)
throttle = self.config["downloads"]["throttledratelimit"]
if throttle:
self.obs["throttledratelimit"] = throttle * 1024
def _build_obs_postprocessors(self):
"""add postprocessor to obs"""
postprocessors = []
if self.config["downloads"]["add_metadata"]:
postprocessors.append(
{
"key": "FFmpegMetadata",
"add_chapters": True,
"add_metadata": True,
}
)
postprocessors.append(
{
"key": "MetadataFromField",
"formats": [
"%(title)s:%(meta_title)s",
"%(uploader)s:%(meta_artist)s",
":(?P<album>)",
],
"when": "pre_process",
}
)
if self.config["downloads"]["add_thumbnail"]:
postprocessors.append(
{
"key": "EmbedThumbnail",
"already_have_thumbnail": True,
}
)
self.obs["writethumbnail"] = True
self.obs["postprocessors"] = postprocessors
def get_format_overwrites(self, youtube_id):
"""get overwrites from single video"""
overwrites = self.video_overwrites.get(youtube_id, False)
if overwrites:
return overwrites.get("download_format", False)
return False
def _dl_single_vid(self, youtube_id):
"""download single video"""
obs = self.obs.copy()
format_overwrite = self.get_format_overwrites(youtube_id)
if format_overwrite:
obs["format"] = format_overwrite
dl_cache = self.config["application"]["cache_dir"] + "/download/"
# check if already in cache to continue from there
all_cached = ignore_filelist(os.listdir(dl_cache))
for file_name in all_cached:
if youtube_id in file_name:
obs["outtmpl"] = os.path.join(dl_cache, file_name)
success = YtWrap(obs, self.config).download(youtube_id)
if self.obs["writethumbnail"]:
# webp files don't get cleaned up automatically
all_cached = ignore_filelist(os.listdir(dl_cache))
to_clean = [i for i in all_cached if not i.endswith(".mp4")]
for file_name in to_clean:
file_path = os.path.join(dl_cache, file_name)
os.remove(file_path)
return success
def move_to_archive(self, vid_dict):
"""move downloaded video from cache to archive"""
videos = self.config["application"]["videos"]
host_uid = self.config["application"]["HOST_UID"]
host_gid = self.config["application"]["HOST_GID"]
channel_name = clean_string(vid_dict["channel"]["channel_name"])
if len(channel_name) <= 3:
# fall back to channel id
channel_name = vid_dict["channel"]["channel_id"]
# make archive folder with correct permissions
new_folder = os.path.join(videos, channel_name)
if not os.path.exists(new_folder):
os.makedirs(new_folder)
if host_uid and host_gid:
os.chown(new_folder, host_uid, host_gid)
# find real filename
cache_dir = self.config["application"]["cache_dir"]
all_cached = ignore_filelist(os.listdir(cache_dir + "/download/"))
for file_str in all_cached:
if vid_dict["youtube_id"] in file_str:
old_file = file_str
old_path = os.path.join(cache_dir, "download", old_file)
new_path = os.path.join(videos, vid_dict["media_url"])
# move media file and fix permission
shutil.move(old_path, new_path, copy_function=shutil.copyfile)
if host_uid and host_gid:
os.chown(new_path, host_uid, host_gid)
@staticmethod
def _delete_from_pending(youtube_id):
"""delete downloaded video from pending index if its there"""
path = f"ta_download/_doc/{youtube_id}"
_, _ = ElasticWrap(path).delete()
def _add_subscribed_channels(self):
"""add all channels subscribed to refresh"""
all_subscribed = PlaylistSubscription().get_playlists()
if not all_subscribed:
return
channel_ids = [i["playlist_channel_id"] for i in all_subscribed]
for channel_id in channel_ids:
self.channels.add(channel_id)
return

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