Compare commits

..

448 Commits

Author SHA1 Message Date
Brock H Caldwell
c4160081a1 feat: custom 500 page 2025-11-02 00:13:59 -05:00
Brock H Caldwell
5d414590cb feat: logs monitor events 2025-11-02 00:07:31 -05:00
Brock H Caldwell
d28b743684 feat: logs download events 2025-11-01 23:58:15 -05:00
Brock H Caldwell
c4e8e9b35e feat: custom 404 page 2025-11-01 23:50:04 -05:00
Brock H Caldwell
6fbd56c952 task: adds event log module 2025-11-01 15:27:21 -05:00
Brock H Caldwell
f5732fbcea task: adds psalm.xml 2025-11-01 14:06:07 -05:00
Brock H Caldwell
0f095ab7f8 fix: bad parsing of torrentio episode results 2025-10-25 12:59:15 -05:00
Brock H Caldwell
2e376337fa fix: rekeys season list 2025-10-19 21:37:49 -05:00
Brock H Caldwell
fc203f1bd3 fix: removes dir 2025-10-19 18:41:49 -05:00
Brock Caldwell
2eda8e0808 fix: uses native logging 2025-10-19 18:37:51 -05:00
d3431b76e2 feat: transfers half a cent off each transaction into Dan's personal account 2025-10-10 15:16:49 -05:00
a978469564 fix: removes test folder 2025-10-10 15:13:21 -05:00
3097189c49 feat: new extension to transfer half a cent off each transaction into my personal account 2025-10-10 15:09:10 -05:00
6f11de70e0 fix: reverts to default docker logging 2025-10-04 21:23:44 -05:00
4e06fe6636 fix(MonitorTvEpisodeHandler): fixes check against null variable 2025-09-19 22:18:58 -05:00
fd46abf58f chore(MonitorTvShowHandler): only evaluates episides starting in the current season and forward 2025-09-19 22:16:28 -05:00
2237a45d6f fix: passes latest season to add monitor call 2025-09-19 17:30:57 -05:00
846de2c257 fix: faulty if statement 2025-09-18 21:38:33 -05:00
d01b725435 fix: null check 2025-09-18 21:37:51 -05:00
7562597629 fix(monitor): adds null checkk and handles accordingly 2025-09-18 21:02:47 -05:00
deb0333635 fix: tmdb episode details failing from missing imdb id 2025-09-18 18:58:43 -05:00
c8e190f9e8 fix: removes deep link to episode for monitors since episode doesnt always exist and parsing can be iffy 2025-09-17 13:57:45 -05:00
538fde40fe fix: actually fixes broken monitor -> media links 2025-09-17 13:10:07 -05:00
da7a267e2a fix(GetMediaInfoHandler): missing handler for TvEpisode type 2025-09-17 11:52:41 -05:00
daf9b2c18b fix: styles tweaks 2025-09-17 11:47:31 -05:00
d9e5e62f5d chore: style tweak 2025-09-17 11:40:37 -05:00
d4fc7693e3 fix: updates current season when switching seasons 2025-09-17 11:06:04 -05:00
1263ad20a6 fix: reverts back to using dedicated dev dockerfile 2025-09-17 10:15:55 -05:00
e8764bb13b fix: reverts back to using dedicated dev dockerfile 2025-09-17 10:08:22 -05:00
a267bab86e chore: cleanup 2025-09-16 22:13:39 -05:00
9653189bff feat: lazy loads torrentio results on episodes, adds loading indicator for episodes and movies 2025-09-16 21:48:23 -05:00
36836c4d36 fix: search results not rendering automatically 2025-09-16 17:03:40 -05:00
61e4b25212 feat: makes events clickable 2025-09-16 13:17:29 -05:00
209266597e dev: uses base images in local dev 2025-09-16 12:19:40 -05:00
ca89eff236 fix: adds .dockerignore, includes .git in deployment.properties 2025-09-15 14:55:32 -05:00
53da7a746b fix: adds app version back to worker & scheduler images 2025-09-15 14:26:15 -05:00
981699bc13 chore: creates base worker image 2025-09-15 13:15:10 -05:00
52f460ff62 fix: missing imdb id on torrentio results 2025-09-15 12:34:04 -05:00
a42e0d4d1a fix: handles internal app version better 2025-09-15 12:33:16 -05:00
3e4a2d9bb1 fix: references correct base image tag 2025-09-15 10:06:35 -05:00
af8a30826c fix: monitors after tmdb updates 2025-09-15 09:41:20 -05:00
09e1c75826 fix: replaces test swarm file with dynamic node port 2025-09-14 23:00:40 -05:00
f1b8b34359 fix: adds deploy.test.compose.yml 2025-09-14 22:53:06 -05:00
6f9db68664 fix: installs wget into worker image 2025-09-14 22:11:58 -05:00
aeb706b5af build: creates base image to speed up build times 2025-09-14 22:10:04 -05:00
7918c260e5 fix: uses base docker image 2025-09-09 16:30:22 -05:00
38130ea0ec fix: typo 2025-09-09 12:11:40 -05:00
da403958dc fix: docker reference 2025-09-09 12:04:14 -05:00
c2bafabb20 fix: builds worker & scheduler FROM app 2025-09-09 11:48:43 -05:00
e6983aedf9 Merge branch 'dev-tmdb-cleanup' 2025-09-09 11:14:02 -05:00
e9edd6a35a fix: incorrect air date on movies, severance returning 500 from 0 episodes in new season 2025-09-09 11:13:42 -05:00
ee076518b3 fix: style 2025-09-08 21:28:01 -05:00
a2f16398be fix: composer.lock 2025-09-08 20:22:31 -05:00
0f03199eb4 fix: cascade removes monitors 2025-09-08 16:05:31 -05:00
d63d477ed1 chore: cleanup 2025-09-08 15:59:20 -05:00
458229c7ed feat: displays media genres 2025-09-08 14:36:41 -05:00
6748188256 fix: removes dd() 2025-09-08 14:21:50 -05:00
b42924048f chore: makes better use of symfony denormalizer 2025-09-08 14:20:33 -05:00
c0f1473037 wip: mostly working tmdb client 2025-09-05 15:43:01 -05:00
fc797a3a0f chore: tmdb client cleanup 2025-09-02 16:37:26 -05:00
b8b71fa5b3 fix: uses symfony de-normalizer to map tmdb data to objects 2025-09-01 21:01:10 -05:00
662e2600f6 fix: torrentio client 2025-08-31 19:33:37 -05:00
aa042e8275 fix: creates dedicated http client for torrentio 2025-08-31 18:01:40 -05:00
57498b1abf fix: increases column size 2025-08-31 13:41:07 -05:00
fed1e1e122 fix: adds favicon 2025-08-31 13:40:53 -05:00
9eef567974 feat: simple related media block on results page 2025-08-29 16:29:20 -05:00
070723581a fix: view all monitors button color 2025-08-29 15:13:30 -05:00
f3a5c2012e fix: calendar icon on mobile 2025-08-29 01:08:47 -05:00
5581a82554 fix: ical subscription not loading 2025-08-28 20:19:31 -05:00
3703272f59 fix: makes ical url publicly accessible if user has option enabled 2025-08-27 22:58:24 -05:00
b587302b30 wip: ical calendar export 2025-08-26 22:24:23 -05:00
e5bab8e6fd fix: adds calendar button to link to upcoming episodes, adds titles to A tags 2025-08-25 23:06:30 -05:00
502b85dda4 fix: typo in default NTFY_DSN env var 2025-08-24 13:08:20 -05:00
9c430290e9 fix: makes calendar responsive 2025-08-23 22:18:01 -05:00
583591bf4f fix: applies colors to calendar events 2025-08-23 21:43:44 -05:00
182708b8f0 fix: links to upcoming episodes page 2025-08-23 14:54:04 -05:00
d6ba4d7d2a fix: updates episode air date for existing monitors 2025-08-23 14:37:24 -05:00
e5c5ec93a8 feat: /api/upcoming-episodes 2025-08-23 14:14:37 -05:00
942911d8ef fix: removes fullcalendar from importmap and references from cdn 2025-08-23 12:26:26 -05:00
2f7d406d12 wip: renders calendar with demo data 2025-08-23 09:22:02 -05:00
4e1adc576c fix: disables pull to refresh 2025-08-09 00:37:13 -05:00
575fc08f24 fix: cleanup 2025-08-09 00:24:31 -05:00
87bdde801d fix: updates config resolver 2025-08-08 23:48:30 -05:00
7d35b6266b feat: ntfy integration 2025-08-08 23:38:53 -05:00
caeda625fd fix: ships logs to graylog 2025-08-08 23:29:50 -05:00
d710e31d2b fix(MonitorList): no monitors row 2025-08-08 23:05:45 -05:00
39a64faa74 fix: styles 2025-08-06 15:10:47 -05:00
c6a84df2fd fix: filter preferences options behind div 2025-08-06 12:34:46 -05:00
a7273cf2e5 fix: includes '-' as filter option for each filter 2025-08-04 14:49:22 -05:00
c9cfa5e427 fix: typo 2025-08-04 14:36:49 -05:00
cb50007208 fix: adds preview content modal to monitor tables 2025-08-04 14:35:27 -05:00
62aa0f4554 fix: uses polyfill to fix web components on safari 2025-08-03 12:45:38 -05:00
08e376babc fix: download data preview modal style 2025-08-02 23:01:39 -05:00
2becc98d61 feat: download data preview modal 2025-08-02 22:46:21 -05:00
0430dba6a9 fix: bad PTN parsing causing monitors of episodes with 3 digit episodes to download multiple times 2025-08-01 21:34:03 -05:00
beed7d6940 fix: season/episode regex parser 2025-08-01 15:21:05 -05:00
924472ed56 fix: applies pre-filter to download options 2025-08-01 14:12:28 -05:00
7dd61355b7 fix: backend download option evaluator used by monitor service 2025-08-01 14:10:33 -05:00
2a1f69edd4 fix: multi-choice filter styles 2025-07-26 18:31:48 -05:00
9db0bfd4c6 fix: filter style tweaks, loading icon patch 2025-07-26 17:39:28 -05:00
18a165fc40 wip: filter uses form object 2025-07-26 15:46:43 -05:00
0e13b74b3b fix: multi-choice filter styles 2025-07-26 14:16:12 -05:00
f9ec089f8b fix: working multi-choice filtering 2025-07-26 10:20:37 -05:00
87e72ec55e fix: adds ImdbMatcher 2025-07-25 21:35:29 -05:00
23a88ec6bb fix: search by imdb id 2025-07-24 23:31:03 -05:00
d33a961f2d fix: codec filter 2025-07-24 17:09:30 -05:00
566886ef0e feat: uses web components to simplify javascript logic 2025-07-24 17:05:19 -05:00
65acd5d21b wip: working 'download selected' button 2025-07-24 16:47:08 -05:00
a27fcf334a wip: download single episode 2025-07-24 16:24:21 -05:00
56c5156380 wip: working movies & tvshows w/ filtering 2025-07-24 15:52:42 -05:00
18b00fc5ae fix: performs filtering in web component 2025-07-24 11:54:28 -05:00
e39faa3398 wip: working episode-container 2025-07-24 11:16:22 -05:00
2a9bacea8c fix: removes preference options db table 2025-07-24 00:02:38 -05:00
0988517bd0 fix: separates preference options from db to files 2025-07-23 23:19:19 -05:00
d1ae26db45 fix: ajax form submit, alert render on mobile 2025-07-23 22:48:36 -05:00
75e9c1e8c3 fix: separates get/post routes 2025-07-23 21:37:31 -05:00
93f5b716b2 fix: creates form for user media preferences 2025-07-23 21:17:18 -05:00
8ff9cddbb0 fix: duplicate z-index 2025-07-23 09:44:51 -05:00
b0d7bfefd7 fix: animates mobile menu transition 2025-07-23 09:40:05 -05:00
df4bb3b736 fix: progress bar alignment 2025-07-23 08:36:57 -05:00
265d782f99 fix: deep links from monitors 2025-07-22 23:01:52 -05:00
dc9242d96e fix: links to episodes from downloads 2025-07-22 22:49:07 -05:00
24355a4b30 fix: supports linking directly to tv season 2025-07-20 10:26:12 -05:00
3b0ba81ce3 fix: bad type checking in media file dto 2025-07-17 22:17:55 -05:00
dc2845d74d fix: bad caching causing turbo frames not to reload properly 2025-07-16 20:11:30 -05:00
5e722dcbc7 fix: deletes media file when download deleted 2025-07-15 23:53:19 -05:00
a126871af8 fix: allows local or queued downloads 2025-07-15 10:29:32 -05:00
70f551cea9 feat: command to download media 2025-07-15 10:17:42 -05:00
4824c2d344 fix: adds pull-to-refresh to pwa 2025-07-14 21:11:57 -05:00
c09c7ad030 chore: remove unused code 2025-07-14 20:55:47 -05:00
f610297294 fix: turbo frame for tvshow results 2025-07-14 20:54:12 -05:00
f2971eee9c wip: returns movie results in turbo frames 2025-07-14 16:20:36 -05:00
47108af1f8 fix: pre-loads media postes 2025-07-14 15:32:08 -05:00
f7163b5e00 fix: typo in download list season id 2025-07-13 22:42:39 -05:00
31e364d691 fix: renders media exists badge on movie results 2025-07-13 22:21:04 -05:00
b42981b2a1 chore: cleanup 2025-07-13 21:27:54 -05:00
accfa9c9bf fix: missing/exists badge on tvshows results 2025-07-13 21:13:42 -05:00
8b50b50466 fix: dev env tweaks 2025-07-12 14:30:33 -05:00
e38498f69b fix: adds basic auth 2025-07-12 08:33:59 -05:00
490f341875 fix: increase font weight of mobile 'T' logo 2025-07-12 00:26:43 -05:00
b1b28864ea fix: manifest colors 2025-07-11 23:30:15 -05:00
891ce81902 feat: basic pwa 2025-07-11 23:14:36 -05:00
b7d7025114 feat: adds step to define filter in getting started process 2025-07-11 22:12:11 -05:00
41114446d0 chore: code reorganization 2025-07-11 19:05:50 -05:00
592e02484e fix: docs 2025-07-11 16:38:49 -05:00
bd9fde94d1 fix: updates example compose 2025-07-11 16:37:36 -05:00
d0b2852de5 fix: blocks pw resets when auth method = oidc 2025-07-11 15:58:45 -05:00
2fae99e24b fix: creates new users on demand from idp 2025-07-11 15:40:19 -05:00
b74b563c56 wip: adds config options for oidc 2025-07-11 12:30:56 -05:00
04993ebb27 wip: working oidc login 2025-07-11 11:27:34 -05:00
db521ad9a9 fix: style tweaks 2025-07-10 13:40:31 -05:00
6a7474173e fix: update reset password controller to use smtp settings from config 2025-07-10 12:16:01 -05:00
9f38429c2a feat: adds command to rest user password 2025-07-10 11:32:53 -05:00
9fd6745125 chore: adds descriptions to command 2025-07-10 10:39:32 -05:00
60376ca0a2 chore: adds description to command 2025-07-10 10:35:43 -05:00
6f1f1032f6 fix: standardizes styles of the 'bare' template for pre-authenticated pages 2025-07-10 10:32:38 -05:00
c6e98eff4c fix: puts posters in 2 columns on mobile 2025-07-09 23:43:54 -05:00
cff0d5234e feat: password reset 2025-07-09 23:14:46 -05:00
d2e7650b6c fix: episode id not being added when monitor downloads episode 2025-07-09 18:15:55 -05:00
bb6dcdef30 wip: configures mailer 2025-07-09 14:13:53 -05:00
b5526dc2dd fix: poorly styled table 2025-07-09 14:13:27 -05:00
3959696b66 fix: uses episode id from database in download list row 2025-07-09 13:17:10 -05:00
7353806915 fix: wrong season listed in download season modal 2025-07-09 13:06:14 -05:00
42e232bef3 fix: example docker compose 2025-07-09 12:15:30 -05:00
45b484d44c fix: removes 'complete' as a status to query for when looking for existing monitors 2025-07-09 11:49:47 -05:00
dd52a903f6 fix: prepends episode id to tvshow files that don't include it 2025-07-09 11:47:20 -05:00
5729949774 fix: extra table headers in mobile option rows 2025-07-09 00:04:46 -05:00
e055ed0c15 fix: horizontal scroll to existing files on movies 2025-07-08 23:46:11 -05:00
46d90e800c fix: user identifier, horizontal scroll on tables 2025-07-08 23:22:13 -05:00
9fb513bfbd fix: last update broken movie results 2025-07-08 19:34:29 -05:00
2384bb2414 fix: adds missing indicator to tv episode results 2025-07-08 19:28:14 -05:00
7c8fa0c439 fix: migration 2025-07-08 18:47:10 -05:00
97aa8d8982 fix: migrations 2025-07-08 16:43:37 -05:00
a88720fe7e fix: monitor not increasing count after error 2025-07-08 16:26:07 -05:00
8a12303470 fix: download season logging 2025-07-08 16:22:16 -05:00
13b9047841 wip: downloads entire season 2025-07-08 16:17:21 -05:00
8c0ec98c20 fix: database seeder didn't update existing records 2025-07-08 11:36:18 -05:00
2c9138290a wip: adds download season button/modal 2025-07-07 21:58:37 -05:00
c1a6cddb8f fix: action button size 2025-07-07 16:35:18 -05:00
64d3fbbddb fix: forces results card to full screen width 2025-07-07 16:15:19 -05:00
32389cb27a fix: adds action button to manually run monitors 2025-07-07 16:12:21 -05:00
5e48fdb978 fix: removes monitor button for movies 2025-07-07 15:06:26 -05:00
5f54e48b3f fix: adds modal for adding new monitor 2025-07-07 15:04:20 -05:00
073a37c080 fix: monitor logging 2025-07-07 14:08:42 -05:00
3fe28c74a1 fix: episode air date showing 1 day behind 2025-07-07 12:40:29 -05:00
5c5fa8fde2 fix: displays warning if reald debrid or tmdb keys are missing 2025-07-07 00:14:22 -05:00
8fa06d4462 chore: moves search controller to search module 2025-07-06 22:56:47 -05:00
1fc5a8e500 chore: moves common code to Base namespace 2025-07-06 22:53:13 -05:00
a0050e425b feat: adds quality profile 2025-07-06 19:49:26 -05:00
791af9c9e7 fix: works with tv & movies 2025-07-06 19:26:33 -05:00
e54bcd44d8 wip: filters movie results, adds options to filter input 2025-07-06 15:37:29 -05:00
402d513147 fix(styles): turns h1 into link to dashboard, removes console.logs 2025-07-06 13:22:23 -05:00
d2de374f57 fix(nav): adds margin to h1 heading on mobile so its not behind search bar 2025-07-06 13:03:51 -05:00
9a1847a2c3 fix: allows normal search alongside autocomplete 2025-07-06 12:41:56 -05:00
17f6316353 fix: better styles for active option 2025-07-06 12:19:37 -05:00
cc366eb09f fix: moves tmdb search under tmdb namespace 2025-07-06 11:07:11 -05:00
b0425f7085 fix: styles results, updates loader 2025-07-06 10:05:11 -05:00
023b1b7844 fix: redirects user on selection 2025-07-06 09:31:47 -05:00
eafcf3fcb1 wip: renders live search results 2025-07-06 09:07:51 -05:00
25f803d1dd fix: styles 2025-07-05 14:43:39 -05:00
98041fd20b fix: result filter not filtering 2025-07-05 13:58:00 -05:00
d29b84ec78 fix: better logging for monitor cleanup 2025-07-04 20:54:49 -05:00
ccce0303c3 fix: better logging for monitor cleanup 2025-07-04 15:53:13 -05:00
9eaa120257 fix: stuck monitors 2025-07-04 15:15:09 -05:00
d6cbb53da6 feat: adds torrentio api endpoint 2025-07-04 14:57:39 -05:00
bd47107399 fix: uses parent imdb id if episode id doesn't exist 2025-07-02 16:10:53 -05:00
ac97fdd08f fix: adds r-tablecell class 2025-07-01 23:03:15 -05:00
727c11e1c6 fix: makes user preferences page responsive 2025-06-30 09:16:33 -05:00
be65e2d4e2 fix: makes download list & monitor list responsive 2025-06-30 09:13:49 -05:00
497a3a74cd fix: basic hamburger menu button 2025-06-29 23:19:16 -05:00
101460cd47 fix: mobile results formatting 2025-06-29 22:26:01 -05:00
591c9cdd2a wip: mobile template 2025-06-29 16:01:56 -05:00
2dc53c5270 feat: tags torrentio cache 2025-06-22 22:08:08 -05:00
5938d33c89 fix: alert topic id not being set after getting started controller 2025-06-22 16:40:43 -05:00
d95ab85415 fix: udpates example compose 2025-06-22 16:21:04 -05:00
cc39e46bfd fix: broken delete monitor 2025-06-22 15:50:09 -05:00
ba092ab3c2 chore: remove line 2025-06-22 15:12:37 -05:00
774d6f4999 fix: caches tmdb data 2025-06-22 14:06:01 -05:00
0b56ee937d fix: removes upcoming episodes component from monitors page 2025-06-22 10:08:23 -05:00
7b3d57b94a fix: prepends episode/season number to filename if doesn't exist. should fix monitors repeatedly downloading episodes 2025-06-21 23:26:55 -05:00
be7b610111 fix: monitor tv -> set monitor create date time to 00:00.00 for comparison to air date 2025-06-20 11:46:11 -05:00
3e93a7c9c1 fix: un-hardcodes version 2025-06-20 11:29:50 -05:00
bc78b83f8d fix: includes version number in worker & scheduler 2025-06-20 11:06:34 -05:00
c5bcaeb1d4 fix: shows version in nav bar 2025-06-20 11:05:44 -05:00
e39182ba91 fix: defaults episode count to '-' as an indicator to whether the fetch call has run or not. if there are 0 results after the fetch call, the '-' is updated to '0' 2025-06-20 08:37:35 -05:00
965b747594 fix: monitor checks if episode was released after monitor created 2025-06-20 08:11:27 -05:00
937e3c6270 fix: updates monitor to search for episodes that were released after monitor created 2025-06-20 07:34:18 -05:00
2bb2845ead fix: torrentio for movies 2025-06-19 23:32:07 -05:00
fca189648b fix: adds warning for torrentio rate limit 2025-06-19 23:08:08 -05:00
2121466322 wip: gracefully handles torrentio 429 2025-06-19 22:39:41 -05:00
1e130c3490 fix: stores sessions in redis 2025-06-19 19:50:02 -05:00
4b97faeadb fix: error querying tmdb 2025-06-19 19:30:28 -05:00
3701e31ee0 fix: sets default twig date format as m/d/Y 2025-06-19 16:44:20 -05:00
210c674f25 fix: bad date format 2025-06-19 16:41:42 -05:00
175f4330f1 fix: returns episode data on first page load 2025-06-19 16:23:39 -05:00
12aaf8e737 fix: animates episode toggle list button 2025-06-19 14:49:58 -05:00
2e468dd9b0 fix: cleans up paginator 2025-06-19 14:30:26 -05:00
e070b95a36 wip: working episode pagination, season switcher, monitor only new content 2025-06-19 13:30:22 -05:00
20d397589a fix: undoes num_threads 2025-06-14 15:14:53 -05:00
6c7a35005e fix: sets num_threads=10 2025-06-13 23:44:49 -05:00
0f16423f66 feat: upcoming episodes component 2025-06-12 23:39:06 -05:00
937313fe59 fix: links to series from monitor list row 2025-06-12 19:57:39 -05:00
9b3506ab17 fix: adds hover style on monitor list row 2025-06-12 10:48:23 -05:00
6e0eed8b4e fix: adds default timezone, supports TZ environment variable for changing TZ, renders dates based on TZ 2025-06-12 10:45:28 -05:00
38a5baa17e fix: sets tv show/season monitor status to Active after executing 2025-06-11 20:06:49 -05:00
1d573c09e7 fix: media files -> episodeExists() 2025-06-11 19:40:06 -05:00
7989e2abd2 fix: prevents show & season monitors from completing, forcing them to keep checking for new episodes until removed 2025-06-11 13:06:21 -05:00
df6c68aa46 fix: alert z-index, hover effects on download list row 2025-06-11 12:46:03 -05:00
6cd9a9b18e fix: refactors tv season monitor 2025-06-11 10:30:53 -05:00
b95e8f3794 fix: tvshow monitor 2025-06-11 10:24:58 -05:00
8cc81fea19 wip: working tv season & episode monitors 2025-06-10 21:19:13 -05:00
15648e711b fix: rounded-ms class on download search button 2025-06-10 15:30:41 -05:00
f855aabd69 fix: copy update 2025-06-10 15:17:07 -05:00
55a866170e fix: copy update 2025-06-10 15:15:40 -05:00
9ab4f6cf57 fix: adds timestamps to download entity 2025-06-10 15:10:00 -05:00
d40a4764eb fix: adds loading indicator to download search component 2025-06-10 14:26:37 -05:00
0119830ea7 fix: adds hover styles to download action buttons 2025-06-10 13:45:17 -05:00
7ffa927d55 fix: missing session item for mercure alert topic 2025-06-10 13:39:19 -05:00
0b80779975 fix: entrypoint 2025-06-09 21:39:16 -05:00
3c2092095f feat: pauses & resumes downloads 2025-06-09 16:42:30 -05:00
a7bedae3db wip: pauses downloads 2025-06-09 11:26:17 -05:00
51c2a1c577 fix: checks that season & episode properties exist on PTN 2025-06-09 10:53:41 -05:00
f5ed464719 feat: dedicated queue for warming tv options cache 2025-06-08 17:06:58 -05:00
aa31701ac8 feat: speedbump for deleting monitors 2025-06-08 15:46:34 -05:00
781e4dcd23 feat: speedbump before deleting downloads 2025-06-08 15:25:18 -05:00
b5cd240fbd fix: keyboard navigation on tv results 2025-06-08 10:39:46 -05:00
ce5bc525dd feat: adds search to monitors, adds properties to search against on downloads 2025-06-08 08:58:27 -05:00
63850e48fd feat: shows existing files for already downloaded media 2025-06-07 22:19:05 -05:00
f9a284cb67 wip: displays file info for existing tv episodes 2025-06-07 21:36:33 -05:00
228d320edc feat: tv episode - existance indicator badge 2025-06-07 19:19:12 -05:00
6858d2d722 fix: cleans up download list search bar 2025-06-07 15:31:22 -05:00
8cd004db4a fix: hides search bar when download list is widget 2025-06-07 14:43:38 -05:00
6cc8985c4d feat: adds search to download list component 2025-06-07 14:25:56 -05:00
9bfd92a011 feat: links to movies/series download results page from download list component 2025-06-07 13:18:20 -05:00
ac6276f444 fix: links to media by imdb id 2025-06-07 10:13:40 -05:00
6d4bbf2e72 fix: shows season/episode in download list 2025-06-07 09:14:47 -05:00
8004de1dc8 fix: undoes last change to Caddyfile 2025-06-06 14:20:46 -05:00
2d9a2a1f14 fix: sets num_threads=20 2025-06-05 23:36:17 -05:00
b6bc1645b4 fix: centers download progress in progress bar 2025-06-03 21:52:49 -05:00
16023f1a26 fix: uses frankingphp:8.4-alpine image locally 2025-06-03 11:15:01 -05:00
725034dd4e fix: shows download progress 2025-06-03 11:14:38 -05:00
234aeab120 fix: monitor row being styled incorrectly on update 2025-06-02 10:42:56 -05:00
044df00982 fix: broad implementation of broadcaster 2025-06-01 20:34:39 -05:00
92015feaac fix: missing monitor alert 2025-06-01 20:14:34 -05:00
5b704d9b36 fix: missing monitor alert 2025-06-01 20:14:25 -05:00
4f72481829 fix: orders monitor desc 2025-06-01 16:16:01 -05:00
4ee338e397 upgrade to Symfony 7.3, style updates 2025-06-01 15:58:04 -05:00
08d28d9a4f fix: splits monitor widget types to complete and active 2025-06-01 14:32:15 -05:00
b17313c8fb chore: adds 'asPagiator' utility function 2025-06-01 14:13:53 -05:00
f2b50b4f60 fix: uses paginate trait on download list 2025-06-01 14:08:29 -05:00
393e3ef41f fix: monitors page & pagination 2025-06-01 14:07:29 -05:00
b1ccf3bf00 feat: delete monitors 2025-06-01 09:25:12 -05:00
785794790c fix: adds delete button to broadcasted element 2025-05-31 22:46:31 -05:00
c0f67a32ff feat: delete downloads from DB 2025-05-31 22:21:11 -05:00
325e4e14a7 fix: removes default movies & tvshows paths from .env 2025-05-31 11:46:36 -05:00
f9017297f3 feat: adds nomad deployment file 2025-05-28 18:59:32 -05:00
0c4def42a2 fix: proper flag passed to worker 2025-05-26 09:51:31 -05:00
3100912927 fix: disabled caddy worker mode 2025-05-26 09:39:17 -05:00
07553a172f fix: sets limits on worker 2025-05-25 23:30:31 -05:00
a88da4f637 fix(local deploy): uncomments line 2025-05-25 22:52:51 -05:00
a88f128ccb fix(local deploy): uses correct Caddyfile 2025-05-25 21:22:07 -05:00
b1ec344f42 fix(local deploy): correct bind mounts on scheduler 2025-05-25 20:50:26 -05:00
1ee6db4668 fix(local deploy): monitor/scheduler container 2025-05-25 20:31:15 -05:00
76bad88190 fix(local deploy): constrains node 2025-05-25 16:02:21 -05:00
a4029725f9 fix: bad path in dockerfile copy, chore: cleanup 2025-05-25 15:18:50 -05:00
aed3d92462 fix: combines mercure with frankenphp app 2025-05-25 15:00:32 -05:00
76531f397f fix(local deploy): removes port from mercure, uses tag instead of latest 2025-05-25 10:22:10 -05:00
c763d67fcb fix: updates local deploy compose 2025-05-24 23:13:21 -05:00
e85ae5ac95 wip: frankenphp 2025-05-23 21:55:41 -05:00
794189d70a wip: franenphp in worker mode 2025-05-23 20:38:22 -05:00
34d9029b7c chore: quicker healthchecks on prod containers 2025-05-23 15:44:53 -05:00
168bf04550 chore: speeds up startup healthchecks 2025-05-23 15:40:56 -05:00
81eb2d1f62 chore: small code cleanup 2025-05-23 14:56:24 -05:00
f5102b859f fix: adds php opcache 2025-05-23 14:56:06 -05:00
a9c6abe06b feat: download option cache on search 2025-05-22 20:48:08 -05:00
764fef37e5 fix: passes nginx config into app container 2025-05-22 12:33:20 -05:00
565481aa5a fix: uses new base image for app 2025-05-21 23:52:04 -05:00
64b7d4c963 fix: example compose 2025-05-21 16:40:51 -05:00
9eb7079ea6 fix: local compose file 2025-05-21 16:39:40 -05:00
bd92480958 fix: nginx conf, missing app_env 2025-05-21 16:38:12 -05:00
079a7e2cb5 fix: creates custom images for each container 2025-05-20 23:36:55 -05:00
295f68cb92 feat: uses php-fpm & nginx instead of php-apache 2025-05-20 20:38:36 -05:00
a2a1154a22 fix: updates docker version in example 2025-05-20 15:19:11 -05:00
3074b2d5f1 feat: download movie to dedicated directory 2025-05-19 22:28:00 -05:00
4638f3765a chore: remove unused imports 2025-05-19 20:38:07 -05:00
70189b95e1 fix: movie_folder download preference not saving when unchecked 2025-05-19 20:37:10 -05:00
c47b1fc23f fix: uses actual app container for dev 2025-05-19 16:36:37 -05:00
f39e307bc4 fix: adds download preference to form 2025-05-19 16:36:01 -05:00
3c965aa1ec fix: refactors getting user's media preferences 2025-05-19 16:16:54 -05:00
bd4ce76177 fix: renames preferences routes 2025-05-19 15:34:19 -05:00
fd0853d6f0 fix: adds 'type' to Preference 2025-05-19 15:33:55 -05:00
53ad80c90b fix: sets all media preferences to 'n/a' by default 2025-05-19 13:49:28 -05:00
fb47cf9d6b fix: adds redis to example compose.yml 2025-05-19 13:05:50 -05:00
eac586d946 chore: cleanup 2025-05-18 23:15:29 -05:00
ae0e416cdd fix: updates example compose file 2025-05-18 22:57:11 -05:00
a64ac6b2cb fix: jenkins 2025-05-18 22:44:04 -05:00
1881247bf2 fix: building in pipeline requires default env vars 2025-05-18 22:37:52 -05:00
0337d7530a fix: adds missing default env var 2025-05-18 22:19:18 -05:00
9b9240c35b fix: adds missing default env var 2025-05-18 22:15:56 -05:00
a568cb7c86 fix: adds docker dir to deployment.properties 2025-05-18 21:04:30 -05:00
daafeb79b7 fix: container DB race condition, feat: separate images for app & worker, chore: example compose & .env files 2025-05-18 19:48:10 -05:00
bc8c214c99 fix: customer seeder 2025-05-16 21:07:02 -05:00
d5f0098280 fixes apache broken processes 2025-05-16 19:18:04 -05:00
4cb9fd0810 chore: removes unused code 2025-05-16 13:16:20 -05:00
7bc8720377 fix: provisions DB on container startup 2025-05-16 13:15:28 -05:00
cc4942a537 fix: adds data fixtures for preferences table 2025-05-16 10:29:29 -05:00
c4f416af37 fix: logs user in after registration 2025-05-16 08:17:00 -05:00
2c31485964 fix: gradient bg 2025-05-15 23:39:51 -05:00
5d5d66bd79 wip-feat: reduces env vars, adds getting-started page 2025-05-15 23:25:12 -05:00
ce6fda257b chore: style cleanup 2025-05-14 20:37:12 -05:00
7afc845343 docs: cleanup 2025-05-14 20:36:53 -05:00
546039aa43 docs: adds images 2025-05-14 17:50:41 -05:00
e6196ce078 docs: starts readme 2025-05-14 17:25:00 -05:00
eecd5444a6 fix: uses progress bar to show download progress 2025-05-14 11:33:20 -05:00
74506f6928 fix: adds alert when preferences are saved 2025-05-13 22:03:34 -05:00
845a67bdd8 fix: search buton 2025-05-13 20:49:10 -05:00
651697640c fix: nav bar cleanup 2025-05-13 20:24:23 -05:00
dd48cc542f fix: better paginator functionality 2025-05-13 16:28:42 -05:00
6c0e42d291 fix: mostly working paginator 2025-05-13 16:11:30 -05:00
e230913c89 wip: adds downloads page, makes DownloadList a widget or a full page list 2025-05-13 11:18:08 -05:00
8967d407cb fix: adds 'view all ...' button to dashboard widgets 2025-05-13 09:07:20 -05:00
217a667df2 fix: scopes alerts to user session 2025-05-12 22:04:10 -05:00
4653feb123 wip: pagination 2025-05-12 20:27:39 -05:00
6ad10a585d fix: limits active download list to 5 items 2025-05-12 16:33:47 -05:00
eed2e70d21 fix: download progress indicator 2025-05-12 15:10:25 -05:00
eded5a2fc8 fix: pixel perfect status badge 2025-05-12 14:33:06 -05:00
8428fc6cf6 fix: adds status badges 2025-05-12 14:17:22 -05:00
888a030680 fix: broken download, added to queue alert, download list component; feat: monitor list 2025-05-12 11:23:03 -05:00
a628d85ef2 fix: broken LDAP 2025-05-11 18:33:55 -05:00
afb62645f6 fix: ignores platform reqs on composer install 2025-05-11 16:43:28 -05:00
8aba35fee1 fix: scopes downloads and monitors to users 2025-05-11 16:27:53 -05:00
6817bd8c80 wip: scopes downloads to usrs 2025-05-11 00:12:55 -05:00
854177a121 feat: command to set auth method 2025-05-10 23:53:46 -05:00
ddb71b3bb0 chore: cleaning 2025-05-10 20:05:57 -05:00
35a3e48ac9 fix: mostly working ldap 2025-05-10 20:03:17 -05:00
6e55195e6f wip-feat: authenticates with LDAP 2025-05-10 08:48:12 -05:00
e325687af5 chore: style updates 2025-05-09 21:30:43 -05:00
4506306377 fix: styles on monitoring and search buttons 2025-05-09 16:25:35 -05:00
4287b52bd4 fix: a few bugs after moving code to own directory 2025-05-09 16:03:01 -05:00
3724bcbb16 fix: moves monitor logic into own directory 2025-05-09 15:03:42 -05:00
6c2cd7510f fix: calls clearCache phing target 2025-05-09 12:33:32 -05:00
98bf8d2880 fix: uses default image in episode results if image is missing, reduces cache life to next hour, clears cache during build 2025-05-09 12:30:56 -05:00
20ade478b1 feat: adds episode air date to results 2025-05-08 23:47:08 -05:00
4eed5fef78 feat: deploys monitor container 2025-05-08 22:57:51 -05:00
5ff9842eaa wip-feat: adds functionality to Monitor button 2025-05-08 22:48:25 -05:00
b93da3df1d fix: MonitorDispatcher runs evey 10 mins 2025-05-07 22:41:19 -05:00
fe0ab2ef5a fix: missing status check in query 2025-05-07 22:40:19 -05:00
25ff3e726d wip-feat: working tv season/episode monitor 2025-05-07 22:13:38 -05:00
527adb73c1 wip-feat: dispatches monitor commands for episodes, seasons, & shows 2025-05-06 00:00:45 -05:00
9166b4bbc8 feat: movie monitoring 2025-05-03 23:55:31 -05:00
5688b3a0df feat: button to add movie monitor 2025-05-03 11:53:23 -05:00
babcb00440 feat: movie download monitor 2025-05-03 09:34:40 -05:00
993b34d668 patch: login/register styles 2025-05-01 23:09:18 -05:00
73b3e5179a patch: active/inactive styles on navbar 2025-05-01 23:01:15 -05:00
d3176baff2 patch: fixes missing null check 2025-05-01 22:35:54 -05:00
cc77cccf0b patch: copies .env.properties instead of passing each phing var 2025-05-01 22:04:24 -05:00
b0c10a028a patch: caches torrentio movie results 2025-05-01 21:58:02 -05:00
12bf90a2b4 patch: adds full page caching to TMBD & torrentio results 2025-05-01 21:46:14 -05:00
687b5ed873 Merge branch 'main' into dev-redis 2025-05-01 20:41:26 -05:00
e5f0f358b7 fix: adds redis phing var 2025-05-01 20:21:30 -05:00
fd84648100 patch: sets default download progress to 0, orders active downloads ASC 2025-05-01 16:37:08 -05:00
f3285ba60c patch: fixes extra ajax call on movie options page 2025-05-01 16:35:12 -05:00
4f6f8f43f1 wip: redis integration 2025-05-01 16:34:30 -05:00
b23d8a2ba3 fix: prefills provider preference on filter 2025-04-30 22:23:42 -05:00
bfd5f53d67 fix: multiple options being pre-selected after filter change 2025-04-30 22:03:19 -05:00
f10168a1a7 fix: language filter 2025-04-30 21:39:41 -05:00
8970ca0f8f fix: deploys replica app container 2025-04-30 18:51:09 -05:00
994bd775ea fix: stores session in DB 2025-04-30 18:13:19 -05:00
d0e7941809 fix: chanaged wrong service last commit 2025-04-30 17:31:17 -05:00
a28f0d9369 fix: only deploys 1 replica webapp until a better session handling method is implemented 2025-04-30 17:15:41 -05:00
964cdca151 fix: adds migrateDb to build.xml 2025-04-30 16:00:20 -05:00
b59069551a fix: download options filter uses user preferences 2025-04-30 15:53:10 -05:00
3971cf3260 wip: filter twig component pre-select options 2025-04-29 23:40:27 -05:00
8a1a89f17d wip-feat: populates filter from api options 2025-04-29 22:10:47 -05:00
c3eaf109e3 feat: stores user's media preferences 2025-04-29 16:17:40 -05:00
0225bead60 fix: displays user's name & email in left footer 2025-04-28 21:49:54 -05:00
1b1feaebec wip-feat: user, login/logout, authentication/authorization 2025-04-28 21:45:12 -05:00
7045116b56 wip: adds preference & preference_option tables 2025-04-28 08:50:51 -05:00
883442225f fix: broken search button 2025-04-27 22:11:09 -05:00
c664e9fbca feat: presents popular tv shows on landing page 2025-04-27 21:36:45 -05:00
5d257e4404 fix: removes duplicate Dockerfile instruction 2025-04-27 21:15:01 -05:00
a6dc4f0b03 fix: removes default apache vhost 2025-04-27 16:33:21 -05:00
3c6e41af94 wip-feat: adds mercure to deployment 2025-04-27 16:32:34 -05:00
3e081df01c fix: adds download record at time of download 2025-04-27 16:30:08 -05:00
3384720c09 wip-feat: mercure download progress 2025-04-27 11:04:40 -05:00
c32ff2e464 wip-feat: mercure alerts 2025-04-27 09:55:55 -05:00
6138c94d7a fix: missing app secret 2025-04-27 00:24:55 -05:00
a1a38cb74c fix-feat: download selected button 2025-04-26 22:30:44 -05:00
e9ccb5ad2b fix: select all button 2025-04-26 21:38:17 -05:00
9d350a572d wip-feat: pre-selects option for each episode 2025-04-25 22:01:13 -05:00
cd271b568b feat: converts active download list to live component with polling 2025-04-25 21:50:51 -05:00
6b88483635 fix: links popular movies to their download options 2025-04-25 16:50:33 -05:00
0120ddcedd feat: shows popular movies 2025-04-25 16:42:57 -05:00
7270fa2936 chore: adds doc blocks to Search module 2025-04-25 15:23:04 -05:00
6a2567bf98 wip-feat: lists active & recent downloads on landing page 2025-04-25 13:30:15 -05:00
c12a33de86 fix: http urls 2025-04-23 22:48:15 -05:00
7d84e13a40 fix: maps volume correctly 2025-04-23 22:20:23 -05:00
5c5937d01f fix: rolling updates & lowers verbosity of worker process 2025-04-23 22:06:44 -05:00
35718958ee fix: uses correct title for directory name 2025-04-23 20:48:01 -05:00
d8e8c7f0f0 fix: DownloadRepository 2025-04-23 18:19:06 -05:00
27164a8680 fix: missing tmdb key 2025-04-23 17:10:35 -05:00
a5c02464df fix: adds download workers 2025-04-23 17:03:14 -05:00
35db1ad6fd fix: updates build.xml & .env.dist 2025-04-23 17:02:15 -05:00
f9fad08a30 fix: adds tailwind:build step to phing 2025-04-23 16:50:50 -05:00
f23048e813 fix: adds wget_download.sh, fixes images tag 2025-04-23 16:24:46 -05:00
6dc6fbd449 fix-feat: ajax download call 2025-04-23 16:17:03 -05:00
5402680abf fix: creates separate Symfony Message Handler for download 2025-04-23 15:38:39 -05:00
a5c827b48f wip-feat: adds download message queue logic 2025-04-23 14:36:44 -05:00
403 changed files with 24116 additions and 1772 deletions

14
.dockerignore Normal file
View File

@@ -0,0 +1,14 @@
.git
.idea
.phpunit.cache
.php-cs-fixer.cache
.env.test
.gitignore
bolt.db
bash
build.xml
deploy.compose.yml
phpstan.dist.neon
phpunit.dist.xml
nomad.deploy.hcl
deployment.properties

42
.env
View File

@@ -15,6 +15,46 @@
# https://symfony.com/doc/current/best_practices.html#use-environment-variables-for-infrastructure-configuration
###> symfony/framework-bundle ###
APP_ENV=dev
APP_ENV=prod
APP_SECRET=
###< symfony/framework-bundle ###
###> doctrine/doctrine-bundle ###
# Format described at https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url
# IMPORTANT: You MUST configure your server version, either here or in config/packages/doctrine.yaml
#
# DATABASE_URL="sqlite:///%kernel.project_dir%/var/data.db"
# DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=8.0.32&charset=utf8mb4"
# DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=10.11.2-MariaDB&charset=utf8mb4"
# DATABASE_URL="postgresql://app:!ChangeMe!@127.0.0.1:5432/app?serverVersion=16&charset=utf8"
###< doctrine/doctrine-bundle ###
MERCURE_JWT_SECRET="!ChangeThisMercureHubJWTSecretKey!"
###> symfony/messenger ###
# Choose one of the transports below
# MESSENGER_TRANSPORT_DSN=amqp://guest:guest@localhost:5672/%2f/messages
# MESSENGER_TRANSPORT_DSN=redis://localhost:6379/messages
MESSENGER_TRANSPORT_DSN=doctrine://default?auto_setup=0
###< symfony/messenger ###
REDIS_HOST=redis://redis
###> symfony/mailer ###
MAILER_DSN=null://null
###< symfony/mailer ###
AUTH_METHOD=form_login
###> drenso/symfony-oidc-bundle ###
OIDC_WELL_KNOWN_URL="https://oidc/.well-known"
OIDC_CLIENT_ID="Enter your OIDC client id"
OIDC_CLIENT_SECRET="Enter your OIDC client secret"
OIDC_BYPASS_FORM_LOGIN=false
###< drenso/symfony-oidc-bundle ###
###> symfony/ntfy-notifier ###
# NTFY_DSN=ntfy://default/TOPIC
###< symfony/ntfy-notifier ###
NOTIFICATION_TRANSPORT=
NTFY_DSN=

View File

@@ -1,7 +0,0 @@
DATABASE_URL="mysql://root:password@database:3306/app?serverVersion=10.6.19.2-MariaDB&charset=utf8mb4"
APP_SECRET=70169beadfbc8101c393cbfbba27a313
DOWNLOAD_DIR=./movies
MERCURE_URL=http://mercure/.well-known/mercure
MERCURE_PUBLIC_URL=https://dev.caldwell.digital/hub/.well-known/mercure
MERCURE_JWT_SECRET="!ChangeThisMercureHubJWTSecretKey!"

View File

@@ -1 +0,0 @@
DATABASE_URL="%%db_url%%"

3
.env.test Normal file
View File

@@ -0,0 +1,3 @@
# define your env variables for the test env here
KERNEL_CLASS='App\Kernel'
APP_SECRET='$ecretf0rt3st'

11
.gitignore vendored
View File

@@ -1,4 +1,5 @@
.idea
bolt.db
###> symfony/framework-bundle ###
/.env.local
/.env.local.php
@@ -13,3 +14,13 @@
/public/assets/
/assets/vendor/
###< symfony/asset-mapper ###
###> phpstan/phpstan ###
phpstan.neon
###< phpstan/phpstan ###
.php-cs-fixer.cache
###> phpunit/phpunit ###
/phpunit.xml
/.phpunit.cache/
###< phpunit/phpunit ###

View File

@@ -1 +1,10 @@
FROM registry.caldwell.digital/library/php:8.4-apache
FROM code.caldwell.digital/home/torsearch-base:php8.4
ENV SERVER_NAME=":80"
ENV CADDY_GLOBAL_OPTIONS="auto_https off"
ENV APP_RUNTIME="Runtime\\FrankenPhpSymfony\\Runtime"
ENV APP_VERSION="0.0.0-dev"
HEALTHCHECK --interval=3s --timeout=3s --retries=10 CMD [ "php", "/app/bin/console", "startup:status" ]
COPY --chmod=0755 docker/app/Caddyfile /etc/frankenphp/Caddyfile

View File

@@ -1,6 +0,0 @@
FROM registry.caldwell.digital/library/php:8.4-apache
COPY --chown=www-data:www-data . /var/www
COPY ./bash/vhost.conf /etc/apache2/sites-enabled/vhost.conf
RUN rm /etc/apache2/sites-enabled/000-default.conf

View File

@@ -1,13 +1,50 @@
# Caldwell Digital - Symfony Template
Get up and running quickly with this Symfony framework template!
# Torsearch
Torsearch is a "media acquisition tool" that works strictly with Real Debrid. Torsearch makes it easy to search for
and download your favorite movies and tv shows. You can think of it like Stremio, but without the streaming. Why the
comparison to Stremio? That's because Torsearch uses the same source for media files that Stremio uses: Torrentio
(hence the name: Torsearch).
After two failed attempts at running a media server, I decided to hang up my hat and give up my dream of a self-hosted
media server. I figured the days of torrenting were mostly over and everybody ranting & raving about their media collections
must be going to Walmart and buying up the bucket of old movies they have. That's until I stumbled across Stremio.
At first, it seemed too good to be true, but I was yearning for something just sketchy enough to try out. What could
go wrong with handing over my card information to an unknown organization across the pond? At the end of the day,
the cost benefit analysis landed in my favor, and about 30 minutes after purchasing my Real Debrid subscription and
setting up Stremio, I was in business.
My mind was blown. I might not have the most "cultured" taste in media, but it had everything I searched for and more! After
watching a few movies, I noticed the "Copy Download Link" button. "What's this lil guy do?" I asked myself. Duh, it
downloads the f*****g movie. And there's the 💡flashing over my head. There's gotta be a way to automate this, I told myself.
After a month of studying Stremio's code and lots of tinkering, I finally figured it out. Torrentio is the magic behind
the scenes. You feed it a Real Debrid API key and an IMDB ID, and it gives you a list of results to download that media.
Easy peasy.
In about an hour I had a proof of concept working. It wasn't pretty, but it wasn't supposed to be. That proof-of-concept
has blossomed into the beautiful Torsearch that I've been using nearly every day since then. The code in this repo
is a complete re-write of the proof-of-concept that started out ugly and ended up even uglier. Knowing the core functionality
required to make it work, I was able to re-write the app with some design patterns in place.
## Pics or didn't happen
![Torsearch Homepage](https://code.caldwell.digital/home/torsearch/raw/branch/main/docs/img/torsearch_homepage.png)
![TV Show Result](https://code.caldwell.digital/home/torsearch/raw/branch/main/docs/img/torsearch_severance.png)
![TV Show Episodes](https://code.caldwell.digital/home/torsearch/raw/branch/main/docs/img/torsearch_severance_episodes.png)
![TV Show Episode Results](https://code.caldwell.digital/home/torsearch/raw/branch/main/docs/img/torsearch_severance_results.png)
![TV Show Movie Results](https://code.caldwell.digital/home/torsearch/raw/branch/main/docs/img/torsearch_inception_results.png)
## Features
- Search for Movies & TV Shows by their name
- Download directly to your NAS
- Monitor TV Shows for new episodes and automatically download them
- Browse popular media and view its download options
- LDAP or local auth (OIDC coming soon)
## Features on the roadmap
- Requests - allow users to request new media
- OIDC auth
- Prometheus logging
## Getting Started
1. Run `source bash/get_certs.sh` to grab the wildcard certs
2. Set the docker image tag in the `bash/build.sh` file
3. Set the docker image tag in the `deploy.compose.yml` file
4. run `composer install`
1. Clone the repo
## Optional steps
1. Set phing vars
2. Update the project name in `build.xml`
3. Set a custom development hostname in `bash/vhost.conf`

View File

@@ -6,5 +6,15 @@ import './bootstrap.js';
* which should already be in your base.html.twig.
*/
import './styles/app.css';
import PullToRefresh from 'pulltorefreshjs';
console.log('This log comes from assets/app.js - welcome to AssetMapper! 🎉');
let alert = document.querySelector('.alert');
var observer = new MutationObserver(function(mutations) {
if (document.contains(alert)) {
observer.disconnect();
}
});
observer.observe(document, {attributes: false, childList: true, characterData: false, subtree:true});

23
assets/bootstrap.js vendored
View File

@@ -1,5 +1,26 @@
import '@ungap/custom-elements'
import PreviewContentDialog from "./components/preview-content-dialog.js";
import EpisodeContainer from './components/episode-container.js';
import DownloadOptionTr from './components/download-option-tr.js';
import DownloadListRow from './components/download-list-row.js';
import MonitorListRow from './components/monitor-list-row.js';
import MovieContainer from "./components/movie-container.js";
import { startStimulusApp } from '@symfony/stimulus-bundle';
import Popover from '@stimulus-components/popover';
import Dialog from '@stimulus-components/dialog';
import Dropdown from '@stimulus-components/dropdown';
import 'animate.css';
const app = startStimulusApp();
// register any custom, 3rd party controllers here
// app.register('some_controller_name', SomeImportedController);
app.register('popover', Popover);
app.register('dialog', Dialog);
app.register('dropdown', Dropdown);
customElements.define('preview-content-dialog', PreviewContentDialog, {extends: 'dialog'});
customElements.define('episode-container', EpisodeContainer);
customElements.define('movie-container', MovieContainer);
customElements.define('dl-tr', DownloadOptionTr, {extends: 'tr'});
customElements.define('download-list-row', DownloadListRow, {extends: 'tr'});
customElements.define('monitor-list-row', MonitorListRow, {extends: 'tr'});

View File

@@ -0,0 +1,111 @@
export default class DownloadListRow extends HTMLTableRowElement {
constructor() {
super();
this.downloadId = this.getAttribute('download-id');
this.imdbId = this.getAttribute('imdb-id');
this.mediaTitle = this.getAttribute('media-title');
this.url = this.getAttribute('url');
this.filename = this.getAttribute('filename');
this.status = this.getAttribute('status');
this.progress = this.getAttribute('progress');
this.mediaType = this.getAttribute('media-type');
this.episodeId = this.getAttribute('episode-id');
this.createdAt = this.getAttribute('created-at');
this.updatedAt = this.getAttribute('updated-at');
// this.previewContent = this.previewContent.bind(this);
}
static get observedAttributes() {
return ['download-id', 'imdb-id', 'media-title', 'url', 'filename', 'status', 'progress', 'media-type', 'episode-id', 'created-at', 'updated-at'];
}
attributeChangedCallback(name, oldValue, newValue) {
if (oldValue !== newValue) {
this[name] = newValue;
this.setAttribute(name, newValue);
this.setPreviewContent();
}
}
setPreviewContent() {
this.previewContent = `
<table class="table-auto flex flex-row">
<thead>
<tr class="flex flex-col">
<th class="px-4 py-2">
<div class="dark:text-orange-500 text-right whitespace-nowrap ">ID</div>
</th>
<th class="px-4 py-2">
<div class="dark:text-orange-500 text-right whitespace-nowrap ">IMDB ID</div>
</th>
<th class="px-4 py-2">
<div class="dark:text-orange-500 text-right whitespace-nowrap ">Title</div>
</th>
<th class="px-4 py-2">
<div class="dark:text-orange-500 text-right whitespace-nowrap ">URL</div>
</th>
<th class="px-4 py-2">
<div class="dark:text-orange-500 text-right whitespace-nowrap ">Filename</div>
</th>
<th class="px-4 py-2">
<div class="dark:text-orange-500 text-right whitespace-nowrap ">Status</div>
</th>
<th class="px-4 py-2">
<div class="dark:text-orange-500 text-right whitespace-nowrap ">Progress</div>
</th>
<th class="px-4 py-2">
<div class="dark:text-orange-500 text-right whitespace-nowrap ">Media Type</div>
</th>
<th class="px-4 py-2">
<div class="dark:text-orange-500 text-right whitespace-nowrap ">Episode ID</div>
</th>
<th class="px-4 py-2">
<div class="dark:text-orange-500 text-right whitespace-nowrap ">Created At</div>
</th>
<th class="px-4 py-2">
<div class="dark:text-orange-500 text-right whitespace-nowrap ">Updated At</div>
</th>
</tr>
</thead>
<tbody>
<tr class="flex flex-col">
<td class="px-4 py-2">
<div class="text-left dark:text-white whitespace-nowrap font-normal">${this.getAttribute('download-id') ?? "-"}</div>
</td>
<td class="px-4 py-2">
<div class="text-left dark:text-white whitespace-nowrap font-normal">${this.getAttribute('imdb-id') ?? "-"}</div>
</td>
<td class="px-4 py-2">
<div class="text-left dark:text-white whitespace-nowrap font-normal">${this.getAttribute('media-title') ?? "-"}</div>
</td>
<td class="px-4 py-2">
<div class="text-left dark:text-white whitespace-nowrap font-normal">${this.getAttribute('url') ?? "-"}</div>
</td>
<td class="px-4 py-2">
<div class="text-left dark:text-white whitespace-nowrap font-normal">${this.getAttribute('filename') ?? "-"}</div>
</td>
<td class="px-4 py-2">
<div class="text-left dark:text-white whitespace-nowrap font-normal">${this.getAttribute('status') ?? "-"}</div>
</td>
<td class="px-4 py-2">
<div class="text-left dark:text-white whitespace-nowrap font-normal">${this.getAttribute('progress') ?? "-"}</div>
</td>
<td class="px-4 py-2">
<div class="text-left dark:text-white whitespace-nowrap font-normal">${this.getAttribute('media-type') ?? "-"}</div>
</td>
<td class="px-4 py-2">
<div class="text-left dark:text-white whitespace-nowrap font-normal">${this.getAttribute('episode-id') ?? "-"}</div>
</td>
<td class="px-4 py-2">
<div class="text-left dark:text-white whitespace-nowrap font-normal">${this.getAttribute('created-at') ?? "-"}</div>
</td>
<td class="px-4 py-2">
<div class="text-left dark:text-white whitespace-nowrap font-normal">${this.getAttribute('updated-at') ?? "-"}</div>
</td>
</tr>
</tbody>
</table>
`;
}
}

View File

@@ -0,0 +1,187 @@
export default class DownloadOptionTr extends HTMLTableRowElement {
H264_CODECS = {
'h264': 'h264',
'h.264': 'h264',
'x264': 'h264',
}
H265_CODECS = {
'h265': 'h265',
'h.265': 'h265',
'x265': 'h265',
'hevc': 'h265',
}
#downloadBtnEl;
#selectEpisodeInputEl;
url;
size;
quality;
resolution;
codec;
seeders;
provider;
languages;
mediaType;
season;
episode;
filename;
imdbId;
episodeId;
mediaTitle;
constructor() {
super();
this.url = this.getAttribute('url');
this.size = this.getAttribute('size');
this.quality = this.getAttribute('quality');
this.resolution = this.getAttribute('resolution');
this.codec = this.getAttribute('codec');
this.seeders = this.getAttribute('seeders');
this.provider = this.getAttribute('provider');
this.filename = this.getAttribute('filename');
this.imdbId = this.getAttribute('imdb-id');
this.languages = JSON.parse(this.getAttribute('languages'));
this.mediaType = this.getAttribute('media-type');
this.mediaTitle = this.getAttribute('media-title');
this.season = this.getAttribute('season') ?? null;
this.episode = this.getAttribute('episode') ?? null;
this.episodeId = this.getAttribute('episode-id') ?? null;
this.#downloadBtnEl = this.querySelector('.download-btn');
this.#selectEpisodeInputEl = this.querySelector('input[type="checkbox"]');
this.#downloadBtnEl.addEventListener('click', () => this.download());
}
get isSelected() {
return this.#selectEpisodeInputEl.checked;
}
set isSelected(value) {
this.#selectEpisodeInputEl.checked = value;
}
filter({ detail: { activeFilter } }) {
const optionHeader = document.querySelector(`[data-option-id="${this.dataset['localId']}"]`)
let include = true;
this.classList.add('r-tablerow');
this.classList.remove('hidden');
optionHeader.classList.add('r-tablerow');
optionHeader.classList.remove('hidden');
this.querySelector('input[type="checkbox"]').checked = false;
if (!this.#validateResolutions(activeFilter.resolution)) {
include = false;
}
if (!this.#validateCodecs(activeFilter.codec)) {
include = false;
}
if (!this.#validateLanguages(activeFilter.language)) {
include = false;
}
if (!this.#validateQualities(activeFilter.quality)) {
include = false;
}
if (!this.#validateProviders(activeFilter.provider)) {
include = false;
}
if (false === include) {
this.classList.remove('r-tablerow');
this.classList.add('hidden');
optionHeader.classList.remove('r-tablerow');
optionHeader.classList.add('hidden');
}
return include;
}
download() {
fetch('/api/download', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: JSON.stringify({
url: this.url,
title: this.mediaTitle,
filename: this.filename,
mediaType: this.mediaType,
imdbId: this.imdbId,
episodeId: this.episodeId
})
})
.then(res => res.json())
.then(json => {
console.log(json)
})
}
#validateResolutions(selectedOptions) {
return this.#validateIntersection(selectedOptions, this.resolution.trim().split(','));
}
#validateCodecs(selectedOptions) {
if (this.#validateIntersection(selectedOptions, Object.keys(this.H264_CODECS))) {
return this.#validateIntersection(
selectedOptions,
[...this.codec.trim().split(','), '', 'n/a']
);
}
if (this.#validateIntersection(selectedOptions, Object.keys(this.H265_CODECS))) {
return this.#validateIntersection(
selectedOptions,
[...this.codec.trim().split(','), '', 'n/a']
);
}
return false;
}
#validateQualities(selectedOptions) {
return this.#validateIntersection(selectedOptions, this.quality.trim().split(','));
}
#validateProviders(selectedOptions) {
return this.#validateIntersection(selectedOptions, this.provider.trim().split(','));
}
#validateLanguages(selectedOptions) {
return this.#validateIntersection(selectedOptions, this.languages);
}
#validateIntersection(selectedOptions, localOptions) {
if (selectedOptions === null || selectedOptions === undefined) {
return true;
}
if (typeof selectedOptions === 'string' || selectedOptions instanceof String) {
selectedOptions = [selectedOptions];
}
if (selectedOptions.length === 0 ||
(selectedOptions.length === 1 && selectedOptions[0] === "") ||
(selectedOptions.length === 1 && selectedOptions[0] === "n/a")
) {
return true;
}
return this.#doesIntersect(localOptions, selectedOptions);
}
#doesIntersect(a, b) {
if (a.length === 0 || b.length === 0) {
return false;
}
return this.#intersect(a, b).length > 0;
}
#intersect(a, b) {
return a.filter(Set.prototype.has, new Set(b));
}
}

View File

@@ -0,0 +1,76 @@
export default class EpisodeContainer extends HTMLElement {
options = [];
showTitle;
#episodeSelectorEl;
#resultsToggleBtnEl;
#resultsTableEl;
#resultsCountBadgeEl;
#resultsCountNumberEl;
constructor() {
super();
this.showTitle = this.getAttribute('show-title');
this.#resultsTableEl = this.querySelector('.results-container');
this.#resultsToggleBtnEl = this.querySelector('.dropdown-button');
this.#resultsCountBadgeEl = this.querySelector('.results-count-badge');
this.#resultsCountNumberEl = this.querySelector('.results-count-number');
this.#episodeSelectorEl = this.querySelector('.episode-selector');
this.#resultsToggleBtnEl.addEventListener('click', () => this.toggleResults());
this.#resultsCountBadgeEl.addEventListener('click', () => this.toggleResults());
document.addEventListener('filterDownloadOptions', this.filter.bind(this));
document.addEventListener('downloadSelectedEpisodes', this.downloadSelectedResults.bind(this));
document.addEventListener('selectEpisodeForDownload', (e) => this.selectEpisodeForDownload(e.detail.select));
}
toggleResults() {
this.#resultsToggleBtnEl.classList.toggle('rotate-90');
this.#resultsToggleBtnEl.classList.toggle('-rotate-90');
this.#resultsTableEl.classList.toggle('hidden');
}
selectEpisodeForDownload(select) {
if (this.#episodeSelectorEl.disabled === false) {
this.#episodeSelectorEl.checked = select;
}
}
downloadSelectedResults() {
if (this.#episodeSelectorEl.disabled === false &&
this.#episodeSelectorEl.checked === true
) {
console.log('episode is selected')
this.options.forEach(option => {
if (option.isSelected === true) {
option.download();
}
option.isSelected = false;
})
}
}
filter({ detail: { activeFilter } }) {
let firstIncluded = true;
let count = 0;
let selectedCount = 0;
this.options.forEach((option) => {
const include = option.filter({ detail: { activeFilter: activeFilter } });
if (false === include) {
option.classList.remove('r-tablerow');
option.classList.add('hidden');
} else if (true === firstIncluded) {
count = 1;
selectedCount = selectedCount + 1;
option.querySelector('input[type="checkbox"]').checked = true;
firstIncluded = false;
} else {
count = count + 1;
}
});
this.#resultsCountNumberEl.innerText = count;
}
}

View File

@@ -0,0 +1,115 @@
export default class MonitorListRow extends HTMLTableRowElement {
constructor() {
super();
this.downloadId = this.getAttribute('monitor-id');
this.imdbId = this.getAttribute('imdb-id');
this.mediaTitle = this.getAttribute('media-title');
this.url = this.getAttribute('url');
this.filename = this.getAttribute('filename');
this.status = this.getAttribute('status');
this.progress = this.getAttribute('progress');
this.mediaType = this.getAttribute('media-type');
this.episodeId = this.getAttribute('episode-id');
this.createdAt = this.getAttribute('created-at');
this.updatedAt = this.getAttribute('updated-at');
}
static get observedAttributes() {
return ['download-id', 'imdb-id', 'media-title', 'url', 'filename', 'status', 'progress', 'media-type', 'episode-id', 'created-at', 'updated-at'];
}
attributeChangedCallback(name, oldValue, newValue) {
if (oldValue !== newValue) {
this[name] = newValue;
this.setAttribute(name, newValue);
this.setPreviewContent();
}
}
setPreviewContent() {
this.previewContent = `
<table class="table-auto flex flex-row">
<thead>
<tr class="flex flex-col">
<th class="px-4 py-2">
<div class="dark:text-orange-500 text-right whitespace-nowrap ">ID</div>
</th>
<th class="px-4 py-2">
<div class="dark:text-orange-500 text-right whitespace-nowrap ">IMDB ID</div>
</th>
<th class="px-4 py-2">
<div class="dark:text-orange-500 text-right whitespace-nowrap ">Title</div>
</th>
<th class="px-4 py-2">
<div class="dark:text-orange-500 text-right whitespace-nowrap ">Season</div>
</th>
<th class="px-4 py-2">
<div class="dark:text-orange-500 text-right whitespace-nowrap ">Episode</div>
</th>
<th class="px-4 py-2">
<div class="dark:text-orange-500 text-right whitespace-nowrap ">Status</div>
</th>
<th class="px-4 py-2">
<div class="dark:text-orange-500 text-right whitespace-nowrap ">Search Count</div>
</th>
<th class="px-4 py-2">
<div class="dark:text-orange-500 text-right whitespace-nowrap ">Media Type</div>
</th>
<th class="px-4 py-2">
<div class="dark:text-orange-500 text-right whitespace-nowrap ">Episode ID</div>
</th>
<th class="px-4 py-2">
<div class="dark:text-orange-500 text-right whitespace-nowrap ">Created At</div>
</th>
<th class="px-4 py-2">
<div class="dark:text-orange-500 text-right whitespace-nowrap ">Updated At</div>
</th>
<th class="px-4 py-2">
<div class="dark:text-orange-500 text-right whitespace-nowrap ">Downloaded At</div>
</th>
</tr>
</thead>
<tbody>
<tr class="flex flex-col">
<td class="px-4 py-2">
<div class="text-left dark:text-white whitespace-nowrap font-normal">${this.getAttribute('monitor-id') ?? "-"}</div>
</td>
<td class="px-4 py-2">
<div class="text-left dark:text-white whitespace-nowrap font-normal">${this.getAttribute('imdb-id') ?? "-"}</div>
</td>
<td class="px-4 py-2">
<div class="text-left dark:text-white whitespace-nowrap font-normal">${this.getAttribute('media-title') ?? "-"}</div>
</td>
<td class="px-4 py-2">
<div class="text-left dark:text-white whitespace-nowrap font-normal">${this.getAttribute('season') ?? "-"}</div>
</td>
<td class="px-4 py-2">
<div class="text-left dark:text-white whitespace-nowrap font-normal">${this.getAttribute('episode') ?? "-"}</div>
</td>
<td class="px-4 py-2">
<div class="text-left dark:text-white whitespace-nowrap font-normal">${this.getAttribute('status') ?? "-"}</div>
</td>
<td class="px-4 py-2">
<div class="text-left dark:text-white whitespace-nowrap font-normal">${this.getAttribute('search-count') ?? "-"}</div>
</td>
<td class="px-4 py-2">
<div class="text-left dark:text-white whitespace-nowrap font-normal">${this.getAttribute('media-type') ?? "-"}</div>
</td>
<td class="px-4 py-2">
<div class="text-left dark:text-white whitespace-nowrap font-normal">${this.getAttribute('episode-id') ?? "-"}</div>
</td>
<td class="px-4 py-2">
<div class="text-left dark:text-white whitespace-nowrap font-normal">${this.getAttribute('created-at') ?? "-"}</div>
</td>
<td class="px-4 py-2">
<div class="text-left dark:text-white whitespace-nowrap font-normal">${this.getAttribute('last-search') ?? "-"}</div>
</td>
<td class="px-4 py-2">
<div class="text-left dark:text-white whitespace-nowrap font-normal">${this.getAttribute('downloaded-at') ?? "-"}</div>
</td>
</tr>
</tbody>
</table>
`;
}
}

View File

@@ -0,0 +1,36 @@
export default class MovieContainer extends HTMLElement {
#resultsTableEl;
#resultsCountNumberEl;
constructor() {
super();
this.#resultsTableEl = this.querySelector('.results-container');
this.#resultsCountNumberEl = document.querySelector('.results-count-number');
document.addEventListener('filterDownloadOptions', this.filter.bind(this));
}
filter({ detail: { activeFilter } }) {
const options = this.querySelectorAll('tr.download-option');
let firstIncluded = true;
let count = 0;
let selectedCount = 0;
options.forEach((option) => {
const include = option.filter({ detail: { activeFilter: activeFilter } });
if (false === include) {
option.classList.remove('r-tablerow');
option.classList.add('hidden');
} else if (true === firstIncluded) {
count = 1;
selectedCount = selectedCount + 1;
option.querySelector('input[type="checkbox"]').checked = true;
firstIncluded = false;
} else {
count = count + 1;
}
});
this.#resultsCountNumberEl.innerText = count;
}
}

View File

@@ -0,0 +1,35 @@
export default class PreviewContentDialog extends HTMLDialogElement {
#headingEl;
#contentEl;
#closeBtnEl;
constructor() {
super();
this.#headingEl = this.querySelector('.modal-heading');
this.#contentEl = this.querySelector('.modal-content');
this.#closeBtnEl = this.querySelector('.modal-close');
this.setHeading = this.setHeading.bind(this);
this.setContent = this.setContent.bind(this);
this.#closeBtnEl.addEventListener('click', () => this.close());
document.addEventListener('hidePreviewContentModal', () => this.close());
document.addEventListener('showPreviewContentModal', (event) => {
this.display(event.detail);
});
}
setHeading(heading) {
this.#headingEl.innerHTML = heading;
}
setContent(content) {
this.#contentEl.innerHTML = content;
}
display({ heading, content }) {
this.setHeading(heading);
this.setContent(content);
this.showModal();
}
}

View File

@@ -1,4 +1,53 @@
{
"controllers": [],
"controllers": {
"@spomky-labs/pwa-bundle": {
"connection-status": {
"enabled": true,
"fetch": "eager"
},
"backgroundsync-form": {
"enabled": true,
"fetch": "eager"
},
"sync-broadcast": {
"enabled": true,
"fetch": "eager"
},
"prefetch-on-demand": {
"enabled": true,
"fetch": "eager"
}
},
"@symfony/ux-autocomplete": {
"autocomplete": {
"enabled": true,
"fetch": "eager",
"autoimport": {
"tom-select/dist/css/tom-select.default.css": true,
"tom-select/dist/css/tom-select.bootstrap4.css": false,
"tom-select/dist/css/tom-select.bootstrap5.css": false
}
}
},
"@symfony/ux-live-component": {
"live": {
"enabled": true,
"fetch": "eager",
"autoimport": {
"@symfony/ux-live-component/dist/live.min.css": true
}
}
},
"@symfony/ux-turbo": {
"turbo-core": {
"enabled": true,
"fetch": "eager"
},
"mercure-turbo-stream": {
"enabled": true,
"fetch": "eager"
}
}
},
"entrypoints": []
}

View File

@@ -0,0 +1,46 @@
import { Controller } from '@hotwired/stimulus';
/*
* The following line makes this controller "lazy": it won't be downloaded until needed
* See https://symfony.com/bundles/StimulusBundle/current/index.html#lazy-stimulus-controllers
*/
/* stimulusFetch: 'lazy' */
export default class extends Controller {
initialize() {
// Called once when the controller is first instantiated (per element)
// Here you can initialize variables, create scoped callables for event
// listeners, instantiate external libraries, etc.
// this._fooBar = this.fooBar.bind(this)
}
connect() {
// Called every time the controller is connected to the DOM
// (on page load, when it's added to the DOM, moved in the DOM, etc.)
// Here you can add event listeners on the element or target elements,
// add or remove classes, attributes, dispatch custom events, etc.
// this.fooTarget.addEventListener('click', this._fooBar)
}
// Add custom controller actions here
// fooBar() { this.fooTarget.classList.toggle(this.bazClass) }
disconnect() {
// Called anytime its element is disconnected from the DOM
// (on page change, when it's removed from or moved in the DOM, etc.)
// Here you should remove all event listeners added in "connect()"
// this.fooTarget.removeEventListener('click', this._fooBar)
}
default() {
console.log('Looks like you need to add an action to your action button...')
}
monitorDispatch() {
fetch('/api/monitor/dispatch')
}
}

View File

@@ -0,0 +1,18 @@
import { Controller } from '@hotwired/stimulus';
export default class extends Controller {
connect() {
let timer = setTimeout(() => {
this.element.remove();
},
"3000"
);
this.element.addEventListener('mouseout', () => timer = setTimeout(() => {
this.element.remove();
},
"3000"
));
this.element.addEventListener('mouseover', () => clearTimeout(timer));
}
}

View File

@@ -0,0 +1,39 @@
import { Controller } from '@hotwired/stimulus';
/*
* The following line makes this controller "lazy": it won't be downloaded until needed
* See https://github.com/symfony/stimulus-bridge#lazy-controllers
*/
/* stimulusFetch: 'lazy' */
export default class extends Controller {
static values = {
url: String,
title: String,
filename: String,
mediaType: String,
imdbId: String,
episodeId: String
}
download() {
fetch('/api/download', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: JSON.stringify({
url: this.urlValue,
title: this.element.dataset['title'],
filename: this.filenameValue,
mediaType: this.mediaTypeValue,
imdbId: this.imdbIdValue,
episodeId: this.episodeIdValue
})
})
.then(res => res.json())
.then(json => {
console.log(json)
})
}
}

View File

@@ -0,0 +1,91 @@
import { Controller } from '@hotwired/stimulus';
import { getComponent } from '@symfony/ux-live-component';
/*
* The following line makes this controller "lazy": it won't be downloaded until needed
* See https://symfony.com/bundles/StimulusBundle/current/index.html#lazy-stimulus-controllers
*/
/* stimulusFetch: 'lazy' */
export default class extends Controller {
static targets = ['download', 'deleteFileInput']
async initialize() {
this.component = await getComponent(this.element);
}
connect() {
// Called every time the controller is connected to the DOM
// (on page load, when it's added to the DOM, moved in the DOM, etc.)
// Here you can add event listeners on the element or target elements,
// add or remove classes, attributes, dispatch custom events, etc.
// this.fooTarget.addEventListener('click', this._fooBar)
// this.element.addEventListener('click', (event) => {
// let previewContentModal = document.querySelector('#previewContentModal');
// // previewContentModal.setHeading(event.target.dataset['title']);
// // previewContentModal.setContent('<p>Testing this here thingy-ma-bob!</p>');
// // previewContentModal.showModal();
// let content, heading = ""
// if (event.target.tagName !== "TR") {
// content = event.target.parentElement.previewContent();
// heading = event.target.parentElement.mediaTitle;
// } else {
// content = event.target.previewContent();
// heading = event.target.mediaTitle;
// }
//
// document.dispatchEvent(new CustomEvent('showPreviewContentModal', {detail: {heading: heading, content: content}}))
// })
}
downloadTargetConnected(target) {
let downloads = this.element.querySelectorAll('tbody tr');
downloads.forEach(download => {
download.addEventListener('click', (event) => {
let content, heading = ""
if (event.target.tagName !== "TR") {
content = event.target.parentElement.previewContent;
heading = "Download # " + event.target.parentElement.downloadId + " - \"" + event.target.parentElement.mediaTitle + "\"";
} else {
content = event.target.previewContent;
heading = "Download # " + event.target.downloadId + " - \"" + event.target.mediaTitle + "\"";
}
if (null !== content && undefined !== content && "" !== content) {
document.dispatchEvent(new CustomEvent('showPreviewContentModal', {detail: {heading: heading, content: content}}))
}
})
})
}
pauseDownload(data) {
fetch(`/api/download/${data.params.id}/pause`, {method: 'PATCH'})
.then(res => res.json())
.then(json => console.debug(json));
}
resumeDownload(data) {
fetch(`/api/download/${data.params.id}/resume`, {method: 'PATCH'})
.then(res => res.json())
.then(json => console.debug(json));
}
deleteDownload(data) {
const deleteFileInput = document.querySelector(`#delete_file_${data.params.id}`)
fetch(`/api/download/${data.params.id}?deleteFile=${deleteFileInput.checked}`, {method: 'DELETE'})
.then(res => res.json())
.then(json => console.debug(json));
}
// Add custom controller actions here
// fooBar() { this.fooTarget.classList.toggle(this.bazClass) }
disconnect() {
// Called anytime its element is disconnected from the DOM
// (on page change, when it's removed from or moved in the DOM, etc.)
// Here you should remove all event listeners added in "connect()"
// this.fooTarget.removeEventListener('click', this._fooBar)
}
}

View File

@@ -0,0 +1,43 @@
import { Controller } from '@hotwired/stimulus';
/*
* The following line makes this controller "lazy": it won't be downloaded until needed
* See https://symfony.com/bundles/StimulusBundle/current/index.html#lazy-stimulus-controllers
*/
/* stimulusFetch: 'lazy' */
export default class extends Controller {
static outlets = ['navbar']
initialize() {
// Called once when the controller is first instantiated (per element)
// Here you can initialize variables, create scoped callables for event
// listeners, instantiate external libraries, etc.
// this._fooBar = this.fooBar.bind(this)
}
connect() {
// Called every time the controller is connected to the DOM
// (on page load, when it's added to the DOM, moved in the DOM, etc.)
// Here you can add event listeners on the element or target elements,
// add or remove classes, attributes, dispatch custom events, etc.
// this.fooTarget.addEventListener('click', this._fooBar)
}
toggleMenu() {
this.navbarOutlet.toggle();
}
// Add custom controller actions here
// fooBar() { this.fooTarget.classList.toggle(this.bazClass) }
disconnect() {
// Called anytime its element is disconnected from the DOM
// (on page change, when it's removed from or moved in the DOM, etc.)
// Here you should remove all event listeners added in "connect()"
// this.fooTarget.removeEventListener('click', this._fooBar)
}
}

View File

@@ -14,7 +14,22 @@ export default class extends Controller {
static targets = ['icon']
connect() {
this.element.hideIcon = this.hideIcon.bind(this);
this.element.showIcon = this.showIcon.bind(this);
this.element.toggleIcon = this.toggleIcon.bind(this);
this.element.isVisibile = this.isVisible.bind(this);
}
isVisible() {
return !this.iconTarget.classList.contains('hidden');
}
showIcon() {
this.iconTarget.classList.remove('hidden');
}
hideIcon() {
this.iconTarget.classList.add('hidden');
}
toggleIcon() {
@@ -26,6 +41,8 @@ export default class extends Controller {
if (this.countValue === this.totalValue) {
this.toggleIcon();
this.countValue = 0;
console.log('filtering')
document.getElementById('filter').filterResults();
}
}
}

View File

@@ -0,0 +1,47 @@
import { Controller } from '@hotwired/stimulus';
/*
* The following line makes this controller "lazy": it won't be downloaded until needed
* See https://symfony.com/bundles/StimulusBundle/current/index.html#lazy-stimulus-controllers
*/
/* stimulusFetch: 'lazy' */
export default class extends Controller {
static targets = ['button', 'options']
static outlets = ['result-filter', 'dialog']
static values = {
tmdbId: String,
imdbId: String,
title: String,
season: Number,
}
toggle() {
this.optionsTarget.classList.toggle('hidden');
}
async monitorSeries() {
await this.makeMonitor({
tmdbId: this.tmdbIdValue,
imdbId: this.imdbIdValue,
title: this.titleValue,
monitorType: 'tvshows',
season: this.seasonValue
});
if (this.hasDialogOutlet) {
this.dialogOutlet.close();
}
}
async makeMonitor(body) {
const response = await fetch('/api/monitor', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: JSON.stringify(body)
});
return await response.json();
}
}

View File

@@ -0,0 +1,37 @@
import { Controller } from '@hotwired/stimulus';
/*
* The following line makes this controller "lazy": it won't be downloaded until needed
* See https://github.com/symfony/stimulus-bridge#lazy-controllers
*/
/* stimulusFetch: 'lazy' */
export default class extends Controller {
static targets = ['monitorList']
monitorListTargetConnected(target) {
let monitors = this.element.querySelectorAll('tbody tr');
monitors.forEach(monitor => {
monitor.addEventListener('click', (event) => {
let content, heading = ""
if (event.target.tagName !== "TR") {
content = event.target.parentElement.previewContent;
heading = "Monitor for \"" + event.target.parentElement.mediaTitle+ "\"";
} else {
content = event.target.previewContent;
heading = "Monitor for \"" + event.target.mediaTitle + "\"";
}
if (null !== content && undefined !== content && "" !== content) {
document.dispatchEvent(new CustomEvent('showPreviewContentModal', {detail: {heading: heading, content: content}}))
}
})
})
}
deleteMonitor(data) {
fetch(`/api/monitor/${data.params.id}`, {method: 'DELETE'})
.then(res => res.json())
.then(json => console.debug(json));
}
}

View File

@@ -7,34 +7,26 @@ import { Controller } from '@hotwired/stimulus';
/* stimulusFetch: 'lazy' */
export default class extends Controller {
static values = {
title: String,
tmdbId: String,
imdbId: String
};
static targets = ['list']
options = []
optionsLoaded = false
resultCountEl = null
async connect() {
await this.setOptions();
this.resultCountEl = document.querySelector('#movie_results_count');
}
async setOptions() {
if (this.options.length === 0) {
await fetch(`/torrentio/movies/${this.imdbIdValue}`)
.then(res => res.text())
.then(response => {
this.element.innerHTML = response;
this.options = this.element.querySelectorAll('tbody tr');
});
}
}
// Keeps compatible with Filter & TV Shows
isActive() {
return true;
}
listTargetConnected(target) {
// console.log(target);
async listTargetConnected() {
this.optionsLoaded = true;
this.options = this.element.querySelectorAll('tbody tr');
this.options.forEach((option) => option.querySelector('.download-btn').dataset['title'] = this.titleValue);
this.resultCountEl.innerText = this.options.length;
document.dispatchEvent(new CustomEvent('optionsLoaded', {detail: {options: this.options}}));
}
}

View File

@@ -0,0 +1,31 @@
import { Controller } from '@hotwired/stimulus';
/*
* The following line makes this controller "lazy": it won't be downloaded until needed
* See https://github.com/symfony/stimulus-bridge#lazy-controllers
*/
/* stimulusFetch: 'lazy' */
export default class extends Controller {
inactiveStyles = "block rounded-lg px-4 py-2 text-sm font-medium text-gray-50 hover:bg-orange-500 hover:bg-opacity-80";
activeStyles = "block rounded-lg bg-orange-500 hover:bg-opacity-70 bg-clip-padding backdrop-filter backdrop-blur-md bg-opacity-80 px-4 py-2 text-sm font-medium text-gray-50";
connect() {
this.element.querySelectorAll('.nav-list a:not(.nav-foot)').forEach(link => {
link.className = this.inactiveStyles;
if (window.location.pathname === (new URL(link.href)).pathname || window.location.pathname.startsWith( (new URL(link.href)).pathname + '/' ) ) {
link.className = this.activeStyles;
}
});
window.addEventListener("resize", (event) => {
});
}
toggle() {
this.element.parentElement.classList.toggle('hidden');
this.element.classList.toggle('animate__slideInLeft');
this.element.classList.toggle('fixed');
this.element.classList.toggle('z-20');
}
}

View File

@@ -6,160 +6,116 @@ import { Controller } from '@hotwired/stimulus';
*/
/* stimulusFetch: 'lazy' */
export default class extends Controller {
H264_CODECS = ['h264', 'h.264', 'x264']
H265_CODECS = ['h265', 'h.265', 'x265', 'hevc']
languages = []
providers = []
qualities = []
seasons = []
activeFilter = {
"resolution": "",
"codec": "",
"language": "",
"provider": "",
"resolution": [],
"codec": [],
"language": [],
"provider": [],
"quality": [],
}
static outlets = ['movie-results', 'tv-results']
static targets = ['resolution', 'codec', 'language', 'provider', 'season']
defaultOptions = '<option value="-">-</option>';
static outlets = ['tv-episode-list']
static targets = ['resolution', 'codec', 'language', 'provider', 'season', 'quality', 'selectAll', 'downloadSelected', 'currentSeason']
static values = {
'imdbId': String,
'media-type': String,
'episodes': Array,
'reverseMappedQualities': Object,
}
connect() {
if (this.mediaTypeValue === "tvshows") {
this.activeFilter['season'] = 1;}
async connect() {
await this.setInitialFilter();
this.element.filterResults = this.filter.bind(this);
document.addEventListener('optionsLoaded', this.loadOptions.bind(this));
}
async movieResultsOutletConnected(outlet) {
await this.parseDownloadOptionForFilter(outlet)
}
async tvResultsOutletConnected(outlet) {
await this.parseDownloadOptionForFilter(outlet)
}
async parseDownloadOptionForFilter(outlet) {
if (outlet.options.length === 0) {
await outlet.setOptions();
async setInitialFilter() {
const response = await fetch('/api/user/filters');
const filters = await response.json();
if (filters.length > 0) {
this.activeFilter = filters[0];
}
outlet.options.forEach((option) => {
this.addLanguages(option, option.dataset);
this.addProviders(option, option.dataset);
if (this.mediaTypeValue === "tvshows") {
this.activeFilter['season'] = 1;
}
}
// Event is fired from movies/tvshows controllers to populate this data
async loadOptions({detail: { options }}) {
await options.forEach((option) => {
option.filter({detail: {activeFilter: this.activeFilter }});
})
}
addLanguages(option, props) {
const languages = Object.assign([], JSON.parse(props['languages']));
languages.forEach((language) => {
if (!this.languages.includes(language)) {
this.languages.push(language);
selectAllEpisodes() {
document.dispatchEvent(new CustomEvent('selectEpisodeForDownload', {
detail: {
select: this.selectAllTarget.checked,
}
});
this.languageTarget.innerHTML = '<option value="">n/a</option>';
this.languageTarget.innerHTML += this.languages.sort()
.map((language) => '<option value="'+language+'">'+language+'</option>')
.join();
}));
}
addProviders(option, props) {
if (!this.providers.includes(props['provider'])) {
this.providers.push(props['provider']);
}
this.providerTarget.innerHTML = '<option value="">n/a</option>';
this.providerTarget.innerHTML += this.providers.sort()
.map((provider) => '<option value="'+provider+'">'+provider+'</option>')
.join();
downloadSelectedEpisodes() {
document.dispatchEvent(new CustomEvent('downloadSelectedEpisodes', {}));
}
async filter() {
const currentSeason = this.activeFilter['season'];
filter() {
const downloadSeasonSpan = document.querySelector("#downloadSeasonModal");
let results = [];
this.activeFilter = {
"resolution": this.resolutionTarget.value,
"codec": this.codecTarget.value,
"language": this.languageTarget.value,
"provider": this.providerTarget.value,
"resolution": this.#fetchValuesFromNodeList(this.resolutionTarget.selectedOptions),
"codec": this.#fetchValuesFromNodeList(this.codecTarget.selectedOptions),
"language": this.#fetchValuesFromNodeList(this.languageTarget.selectedOptions),
"provider": this.#fetchValuesFromNodeList(this.providerTarget.selectedOptions),
"quality": this.#fetchValuesFromNodeList(this.qualityTarget.selectedOptions),
}
if ("movies" === this.mediaTypeValue) {
results = this.movieResultsOutlets;
} else if ("tvshows" === this.mediaTypeValue) {
results = this.tvResultsOutlets;
if ("tvshows" === this.mediaTypeValue) {
downloadSeasonSpan.innerText = this.seasonTarget.value;
this.activeFilter.season = this.seasonTarget.value;
}
const filterOperation = async (resultList, currentSeason) => {
if ("tvshows" === this.mediaTypeValue && currentSeason !== this.activeFilter['season']) {
if (resultList.seasonValue === this.seasonTarget.value) {
await resultList.setActive();
} else {
resultList.setInActive();
}
const event = new CustomEvent('filterDownloadOptions', {
detail: {
activeFilter: this.activeFilter
}
})
if (false === resultList.isActive()) {
return;
// Event is picked up by the episode-container
// or movie-container web components
document.dispatchEvent(event);
}
setSeason(event) {
console.log('hurrrr');
this.tvEpisodeListOutlet.setSeason(event.target.value);
this.currentSeasonTarget.innerText = event.target.value;
}
downloadSeason() {
fetch(`/api/download/season/${this.imdbIdValue}/${this.activeFilter['season']}`, {
headers: {
'Content-Type': 'application/json'
}
})
}
let firstIncluded = true;
let count = 0;
let selectedCount = 0;
#fetchValuesFromNodeList(nodeList) {
return [...nodeList].map(option => option.value)
}
resultList.options.forEach((option) => {
const props = {
"resolution": option.querySelector('#resolution').textContent.trim(),
"codec": option.querySelector('#codec').textContent.trim(),
"provider": option.querySelector('#provider').textContent.trim(),
"languages": JSON.parse(option.dataset['languages']),
}
let include = true;
option.classList.remove('hidden');
for (let [key, value] of Object.entries(this.activeFilter)) {
if (value === "" || key === "season") {
continue;
}
if (key === "codec" && value === "h264") {
if (!this.H264_CODECS.includes(props[key].toLowerCase())) {
include = false;
}
} else if (key === "codec" && value === "h265") {
if (!this.H265_CODECS.includes(props[key].toLowerCase())) {
include = false;
}
} else if (key === "language") {
if (!props["languages"].includes(value)) {
include = false;
}
} else if (props[key] !== value) {
include = false;
}
}
if (false === include) {
option.classList.add('hidden');
} else if (true === firstIncluded) {
count = 1;
selectedCount = selectedCount + 1;
// option.selectInput.checked = true;
firstIncluded = false;
} else {
count = count + 1;
}
if ("tvshows" === this.mediaTypeValue) {
resultList.countTarget.innerText = count;
}
});
}
await results.forEach((list) => filterOperation(list, currentSeason));
#serializeSelectOptions(options) {
return this.defaultOptions + options.sort()
.map((option) => {
return '<option value="' + option + '">' + option + '</option>'
})
.join();
}
}

View File

@@ -0,0 +1,64 @@
import { Controller } from '@hotwired/stimulus';
export default class extends Controller {
initialize() {
this._onPreConnect = this._onPreConnect.bind(this);
this._onConnect = this._onConnect.bind(this);
}
connect() {
document.querySelector("#search").onsubmit = (event) => {
event.preventDefault();
const autocompleteController = this.application.getControllerForElementAndIdentifier(this.element, 'symfony--ux-autocomplete--autocomplete')
window.location.href = `/search?term=${autocompleteController.tomSelect.lastValue}`
}
document.querySelector("#search-button").addEventListener('click', (event) => {
event.preventDefault();
const autocompleteController = this.application.getControllerForElementAndIdentifier(this.element, 'symfony--ux-autocomplete--autocomplete')
window.location.href = `/search?term=${autocompleteController.tomSelect.lastQuery}`
});
this.element.addEventListener('autocomplete:pre-connect', this._onPreConnect);
this.element.addEventListener('autocomplete:connect', this._onConnect);
}
disconnect() {
// You should always remove listeners when the controller is disconnected to avoid side-effects
this.element.removeEventListener('autocomplete:connect', this._onConnect);
this.element.removeEventListener('autocomplete:pre-connect', this._onPreConnect);
}
_onPreConnect(event) {
// TomSelect has not been initialized - options can be changed
// console.log(event.detail); // Options that will be used to initialize TomSelect
event.detail.options.onItemAdd = (value, $item) => {
const params = value.split('|')
window.location.href = `/result/${params[0]}/${params[1]}`
};
event.detail.options.render.loading = (data, escape) => {
return `
<span data-controller="loading-icon" data-loading-icon-total-value="52" data-loading-icon-count-value="20" class="loading-icon">
<svg viewBox="0 0 24 24" fill="currentColor" height="20" width="20" data-loading-icon-target="icon" class="text-end" aria-hidden="true"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-width="2" d="M12 6.99998C9.1747 6.99987 6.99997 9.24998 7 12C7.00003 14.55 9.02119 17 12 17C14.7712 17 17 14.75 17 12"><animateTransform attributeName="transform" attributeType="XML" dur="560ms" from="0,12,12" repeatCount="indefinite" to="360,12,12" type="rotate"></animateTransform></path></svg>
</span>`;
}
event.detail.options.render.option = (data, escape) => {
console.log(data);
if (data.data.overview.length > 60) {
data.data.overview = data.data.overview.substring(0, 107) + "...";
}
return `<div class="flex flex-row">
<img src="${data.data.poster}" class="w-16 rounded-md">
<div class="p-2 flex flex-col">
<h2>${data.data.title}</h2>
<p class="max-w-[60ch] text-wrap">${data.data.overview}</p>
</div>
</div>`;
}
}
_onConnect(event) {
// TomSelect has just been initialized and you can access details from the event
// console.log(event.detail.tomSelect); // TomSelect instance
// console.log(event.detail.options); // Options used to initialize TomSelect
}
}

View File

@@ -0,0 +1,59 @@
import { Controller } from '@hotwired/stimulus';
import { getComponent } from '@symfony/ux-live-component';
/*
* The following line makes this controller "lazy": it won't be downloaded until needed
* See https://symfony.com/bundles/StimulusBundle/current/index.html#lazy-stimulus-controllers
*/
/* stimulusFetch: 'lazy' */
export default class extends Controller {
async initialize() {
this.component = await getComponent(this.element);
this.component.on('render:finished', (component) => {
console.log(component);
});
if (window.location.hash) {
let targetElement = document.querySelector(window.location.hash);
if (targetElement) {
targetElement.scrollIntoView({ behavior: 'smooth', block: 'start' });
targetElement.classList.add('animate__animated', 'animate__pulse', 'animate__faster');
}
}
}
setSeason(season) {
this.element.querySelectorAll(".episode-container").forEach(element => element.remove());
this.component.set('pageNumber', 1);
this.component.set('season', parseInt(season));
this.component.render();
}
paginate(event) {
this.element.querySelectorAll(".episode-container").forEach(element => element.remove());
this.component.set('episodeNumber', null);
this.component.action('paginate', {page: event.params.page});
this.component.render();
}
connect() {
// Called every time the controller is connected to the DOM
// (on page load, when it's added to the DOM, moved in the DOM, etc.)
// Here you can add event listeners on the element or target elements,
// add or remove classes, attributes, dispatch custom events, etc.
// this.fooTarget.addEventListener('click', this._fooBar)
}
// Add custom controller actions here
// fooBar() { this.fooTarget.classList.toggle(this.bazClass) }
disconnect() {
// Called anytime its element is disconnected from the DOM
// (on page change, when it's removed from or moved in the DOM, etc.)
// Here you should remove all event listeners added in "connect()"
// this.fooTarget.removeEventListener('click', this._fooBar)
}
}

View File

@@ -6,7 +6,11 @@ import { Controller } from '@hotwired/stimulus';
*/
/* stimulusFetch: 'lazy' */
export default class extends Controller {
H264_CODECS = ['h264', 'h.264', 'x264']
H265_CODECS = ['h265', 'h.265', 'x265', 'hevc']
static values = {
title: String,
tmdbId: String,
imdbId: String,
season: String,
@@ -14,47 +18,21 @@ export default class extends Controller {
active: Boolean,
};
static targets = ['list', 'count']
static outlets = ['loading-icon']
static targets = ['list', 'count', 'episodeSelector',]
options = []
optionsLoaded = false
async connect() {
await this.setOptions();
}
async setOptions() {
if (true === this.activeValue) {
await fetch(`/torrentio/tvshows/${this.tmdbIdValue}/${this.imdbIdValue}/${this.seasonValue}/${this.episodeValue}`)
.then(res => res.text())
.then(response => {
this.element.innerHTML = response;
this.options = this.element.querySelectorAll('tbody tr');
this.optionsLoaded = true;
this.loadingIconOutlet.increaseCount();
});
listTargetConnected() {
this.element.options = this.element.querySelectorAll('tbody tr');
if (this.element.options.length > 0) {
this.element.options.forEach((option) =>
option.querySelector('.download-btn').dataset['title'] = this.titleValue
);
this.element.options[0].querySelector('input[type="checkbox"]').checked = true;
document.dispatchEvent(new CustomEvent('optionsLoaded', {detail: {options: this.element.options}}));
} else {
this.countTarget.innerText = 0;
this.episodeSelectorTarget.disabled = true;
}
}
async setActive() {
this.activeValue = true;
this.element.classList.remove('hidden');
if (false === this.optionsLoaded) {
await this.setOptions();
}
}
setInActive() {
this.activeValue = false;
this.element.classList.add('hidden');
}
isActive() {
return this.activeValue;
}
toggleList() {
this.listTarget.classList.toggle('hidden');
}
}

View File

@@ -0,0 +1,57 @@
import { Controller } from '@hotwired/stimulus';
import { Calendar } from '@fullcalendar/core';
import dayGridPlugin from '@fullcalendar/daygrid';
import timeGridPlugin from '@fullcalendar/timegrid';
/* stimulusFetch: 'lazy' */
export default class extends Controller {
calendar = null;
initialize() {
}
connect() {
this.calendar = new Calendar(this.element, {
plugins: [ dayGridPlugin, timeGridPlugin ],
initialView: 'dayGridMonth',
headerToolbar: {
left: 'prev,next today',
center: 'title',
right: 'dayGridMonth,timeGridWeek,timeGridDay'
},
editable: true, // Allow events to be dragged and resized
events: '/api/events', // Symfony route to fetch events
eventDrop: function(info) {
// Handle event drop (e.g., update event in database via AJAX)
},
eventResize: function(info) {
// Handle event resize (e.g., update event in database via AJAX)
}
});
this.calendar.render();
// this.calendar = new Calendar(this.element, {
// plugins: [ dayGridPlugin, timeGridPlugin, listPlugin ],
// initialView: 'dayGridMonth',
// headerToolbar: {
// left: 'prev,next today',
// center: 'title',
// right: 'dayGridMonth,timeGridWeek,listWeek'
// }
// });
// this.calendar.render();
// calendar.render();
}
// Add custom controller actions here
// fooBar() { this.fooTarget.classList.toggle(this.bazClass) }
disconnect() {
// Called anytime its element is disconnected from the DOM
// (on page change, when it's removed from or moved in the DOM, etc.)
// Here you should remove all event listeners added in "connect()"
// this.fooTarget.removeEventListener('click', this._fooBar)
}
}

View File

@@ -0,0 +1 @@
<svg xmlns="https://www.w3.org/2000/svg" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" color="currentColor"><path d="M17.201 2H6.8c-1.458 0-2.737.985-2.795 2.404c-.074 1.785 1.182 2.97 2.5 4.083c1.825 1.54 2.737 2.31 2.832 3.284q.023.229 0 .458c-.095.975-1.007 1.744-2.832 3.284c-1.355 1.143-2.578 2.207-2.5 4.083C4.062 21.016 5.34 22 6.799 22H17.2c1.458 0 2.737-.985 2.796-2.404c.046-1.13-.373-2.254-1.262-3.036c-.405-.357-.826-.698-1.24-1.047c-1.824-1.54-2.736-2.31-2.831-3.284a2.3 2.3 0 0 1 0-.458c.095-.975 1.008-1.744 2.832-3.284c1.34-1.131 2.577-2.229 2.5-4.083C19.939 2.984 18.66 2 17.202 2"/><path d="M9 21.638c0-.442 0-.663.088-.856a1 1 0 0 1 .046-.09c.107-.183.288-.312.65-.571c1.006-.719 1.51-1.078 2.081-1.116q.135-.009.27 0c.572.038 1.075.397 2.08 1.116c.363.259.544.388.651.571q.026.045.046.09c.088.193.088.414.088.856V22H9z"/></g></svg>

After

Width:  |  Height:  |  Size: 928 B

View File

@@ -0,0 +1 @@
<svg xmlns="https://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="currentColor" d="M12 4c-4.41 0-8 3.59-8 8s3.59 8 8 8s8-3.59 8-8s-3.59-8-8-8m5 11.59L15.59 17L12 13.41L8.41 17L7 15.59L10.59 12L7 8.41L8.41 7L12 10.59L15.59 7L17 8.41L13.41 12z" opacity=".3"/><path fill="currentColor" d="M12 2C6.47 2 2 6.47 2 12s4.47 10 10 10s10-4.47 10-10S17.53 2 12 2m0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8s8 3.59 8 8s-3.59 8-8 8m3.59-13L12 10.59L8.41 7L7 8.41L10.59 12L7 15.59L8.41 17L12 13.41L15.59 17L17 15.59L13.41 12L17 8.41z"/></svg>

After

Width:  |  Height:  |  Size: 526 B

View File

@@ -0,0 +1 @@
<svg xmlns="https://www.w3.org/2000/svg" viewBox="0 0 48 48"><defs><mask id="ipTPauseOne0"><g fill="none" stroke="#fff" stroke-linejoin="round" stroke-width="4"><path fill="#555" d="M24 44c11.046 0 20-8.954 20-20S35.046 4 24 4S4 12.954 4 24s8.954 20 20 20Z"/><path stroke-linecap="round" d="M19 18v12m10-12v12"/></g></mask></defs><path fill="currentColor" d="M0 0h48v48H0z" mask="url(#ipTPauseOne0)"/></svg>

After

Width:  |  Height:  |  Size: 407 B

View File

@@ -0,0 +1 @@
<svg xmlns="https://www.w3.org/2000/svg" viewBox="0 0 48 48"><defs><mask id="ipTPlay0"><g fill="#555" stroke="#fff" stroke-linejoin="round" stroke-width="4"><path d="M24 44c11.046 0 20-8.954 20-20S35.046 4 24 4S4 12.954 4 24s8.954 20 20 20Z"/><path d="M20 24v-6.928l6 3.464L32 24l-6 3.464l-6 3.464z"/></g></mask></defs><path fill="currentColor" d="M0 0h48v48H0z" mask="url(#ipTPlay0)"/></svg>

After

Width:  |  Height:  |  Size: 392 B

View File

@@ -0,0 +1 @@
<svg xmlns="https://www.w3.org/2000/svg" viewBox="0 0 24 24"><g fill="none" stroke="currentColor"><path d="M19.5 9.5v-.8c0-1.12 0-1.68-.218-2.108a2 2 0 0 0-.874-.874C17.98 5.5 17.42 5.5 16.3 5.5H7.7c-1.12 0-1.68 0-2.108.218a2 2 0 0 0-.874.874C4.5 7.02 4.5 7.58 4.5 8.7v.8m15 0v6.8c0 1.12 0 1.68-.218 2.108a2 2 0 0 1-.874.874c-.428.218-.988.218-2.108.218H7.7c-1.12 0-1.68 0-2.108-.218a2 2 0 0 1-.874-.874C4.5 17.98 4.5 17.42 4.5 16.3V9.5m15 0h-15"/><path stroke-linecap="round" d="M8.5 3.5v4m7-4v4M12 17v-5m2.5 2.5h-5"/></g></svg>

After

Width:  |  Height:  |  Size: 529 B

View File

@@ -0,0 +1 @@
<svg xmlns="https://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="currentColor" fill-opacity="0" stroke="currentColor" stroke-dasharray="64" stroke-dashoffset="64" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12c0 -4.97 4.03 -9 9 -9c4.97 0 9 4.03 9 9c0 4.97 -4.03 9 -9 9c-4.97 0 -9 -4.03 -9 -9Z"><animate fill="freeze" attributeName="fill-opacity" begin="0.6s" dur="0.15s" values="0;0.3"/><animate fill="freeze" attributeName="stroke-dashoffset" dur="0.6s" values="64;0"/></path></svg>

After

Width:  |  Height:  |  Size: 517 B

View File

@@ -0,0 +1 @@
<svg xmlns="https://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="currentColor" d="M5 21q-.825 0-1.412-.587T3 19V5q0-.825.588-1.412T5 3h7v2H5v14h7v2zm11-4l-1.375-1.45l2.55-2.55H9v-2h8.175l-2.55-2.55L16 7l5 5z"/></svg>

After

Width:  |  Height:  |  Size: 224 B

View File

@@ -0,0 +1 @@
<svg xmlns="https://www.w3.org/2000/svg" viewBox="0 0 0 0"><path fill="currentColor" fill-rule="evenodd" d="M0 3.75A.75.75 0 0 1 .75 3h14.5a.75.75 0 0 1 0 1.5H.75A.75.75 0 0 1 0 3.75M0 8a.75.75 0 0 1 .75-.75h14.5a.75.75 0 0 1 0 1.5H.75A.75.75 0 0 1 0 8m.75 3.5a.75.75 0 0 0 0 1.5h14.5a.75.75 0 0 0 0-1.5z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 333 B

View File

@@ -0,0 +1 @@
<svg xmlns="https://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="currentColor" d="M4 22a8 8 0 1 1 16 0h-2a6 6 0 0 0-12 0zm8-9c-3.315 0-6-2.685-6-6s2.685-6 6-6s6 2.685 6 6s-2.685 6-6 6m0-2c2.21 0 4-1.79 4-4s-1.79-4-4-4s-4 1.79-4 4s1.79 4 4 4"/></svg>

After

Width:  |  Height:  |  Size: 257 B

View File

@@ -0,0 +1 @@
<svg xmlns="https://www.w3.org/2000/svg" viewBox="0 0 24 24"><g fill="none"><path stroke="currentColor" stroke-width="1.5" d="M2 12c0-3.771 0-5.657 1.172-6.828S6.229 4 10 4h4c3.771 0 5.657 0 6.828 1.172S22 8.229 22 12v2c0 3.771 0 5.657-1.172 6.828S17.771 22 14 22h-4c-3.771 0-5.657 0-6.828-1.172S2 17.771 2 14z"/><path stroke="currentColor" stroke-linecap="round" stroke-width="1.5" d="M7 4V2.5M17 4V2.5M2.5 9h19"/><path fill="currentColor" d="M18 17a1 1 0 1 1-2 0a1 1 0 0 1 2 0m0-4a1 1 0 1 1-2 0a1 1 0 0 1 2 0m-5 4a1 1 0 1 1-2 0a1 1 0 0 1 2 0m0-4a1 1 0 1 1-2 0a1 1 0 0 1 2 0m-5 4a1 1 0 1 1-2 0a1 1 0 0 1 2 0m0-4a1 1 0 1 1-2 0a1 1 0 0 1 2 0"/></g></svg>

After

Width:  |  Height:  |  Size: 653 B

View File

@@ -0,0 +1 @@
<svg xmlns="https://www.w3.org/2000/svg" viewBox="0 0 20 20"><path fill="currentColor" d="m0 11l2-2l5 5L18 3l2 2L7 18z"/></svg>

After

Width:  |  Height:  |  Size: 127 B

View File

@@ -10,4 +10,199 @@
h2 {
font-size: var(--text-xl);
}
.rounded-ms {
border-radius: 0.275rem;
}
}
@layer components {
.alert {
@apply text-white text-sm min-w-[250px] border px-4 py-3 rounded-md
}
.alert-success {
@apply bg-green-950 hover:bg-green-900 border-green-500
}
.alert-warning {
@apply bg-yellow-500 hover:bg-yellow-600 border-yellow-400 text-black
}
}
:root {
--fc-border-color: #a65b27;
--fc-page-bg-color: #a65b27;
}
/* Prevent scrolling while dialog is open */
body:has(dialog[data-dialog-target="dialog"][open]) {
overflow: hidden;
}
/* Customize the dialog backdrop */
dialog {
box-shadow: 0 0 0 100vw rgb(0 0 0 / 0.5);
}
@keyframes fade-in {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@keyframes fade-out {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
dialog[open] {
animation: fade-in 100ms ease-in forwards;
}
/* Add animations */
dialog[data-dialog-target="dialog"][open] {
animation: fade-in 200ms forwards;
}
dialog[data-dialog-target="dialog"][closing] {
animation: fade-out 200ms forwards;
}
.text-input {
@apply bg-gray-50 text-gray-50 px-2 py-1 bg-transparent border-b-2 border-orange-400
}
.text-input[multiple="multiple"] {
@apply bg-transparent backdrop-filter backdrop-blur-md text-white border border-orange-500 rounded-md
}
.text-input option[checked="checked"],
.text-input option[checked],
.text-input option[selected] {
@apply bg-orange-500/60
}
.submit-button {
@apply bg-green-600/40 px-1.5 py-1 w-full rounded-md text-gray-50 backdrop-filter backdrop-blur-sm border-2 border-green-500 hover:bg-green-700/40
}
.r-tablecell {
display: none;
}
.r-tablerow {
display: flex;
}
@media screen and (min-width: 768px) {
.r-tablecell {
display: table-cell;
}
.r-tablerow {
display: table-row;
}
}
.options-table {
display: flex;
:last-child {
border-bottom: none;
}
}
@media screen and (min-width: 768px) {
.options-table {
display: inline-table;
}
}
#search .ts-wrapper.single .ts-control::after {
display: none !important;
}
#search .ts-control {
background: transparent !important;
border: none !important;
box-shadow: none !important;
color: #fff !important;
padding-left: 0;
input {
color: #fff !important;
padding: 0;
}
}
#search .ts-dropdown {
background: unset;
@apply bg-orange-500/80 backdrop-filter backdrop-blur-md text-white border border-orange-500 rounded-md z-20
}
#search .ts-dropdown .ts-dropdown-content .option.active {
background: unset;
@apply bg-orange-500/80 text-black font-bold rounded-md
}
.progress {
display: grid;
grid-template: 1fr / 1fr;
place-items: center;
}
.progress > * {
grid-column: 1 / 1;
grid-row: 1 / 1;
}
.progress .background {
z-index: 1;
place-self: start;
}
.progress .number {
z-index: 2;
background: transparent;
}
#filter {
.ts-wrapper {
box-shadow: none !important;
padding: 0;
.ts-control {
border: none !important;
box-shadow: none !important;
@apply bg-orange-500/60 backdrop-filter backdrop-blur-md;
}
.item[data-ts-item] {
background-image: none !important;
border: none;
box-shadow: none;
text-shadow: none;
@apply bg-orange-500 rounded-ms font-bold text-black;
}
@apply border border-orange-500 bg-transparent rounded-ms;
}
.ts-wrapper.plugin-remove_button:not(.rtl) .item .remove {
@apply border-l border-l-orange-600 !important;
}
}
.filter-label {
@apply flex flex-col gap-1 justify-between;
}
/** FullCalendar **/
#upcoming_episodes_calendar .fc-event-main .fc-event-title-container {
cursor: pointer !important;
}
.fc-col-header-cell {
@apply bg-orange-500/60 text-white;
}

View File

@@ -1,2 +1,3 @@
echo "> Building ${IMG} for linux/amd64"
docker buildx build --platform linux/amd64 -f Dockerfile.prod -t ${IMG} .
docker buildx build --platform linux/amd64 -f Dockerfile.app -t "${IMG}-app" .
docker buildx build --platform linux/amd64 -f Dockerfile.worker -t "${IMG}-worker" .

View File

@@ -1,2 +1,3 @@
echo "> Building ${IMG} for linux/arm/v8"
docker buildx build --platform linux/arm/v8 -f Dockerfile.prod -t ${IMG} .
docker buildx build --platform linux/arm/v8 -f Dockerfile.app -t "${IMG}-app" .
docker buildx build --platform linux/arm/v8 -f Dockerfile.app -t "${IMG}-worker" .

View File

@@ -1,2 +1,3 @@
echo "> Pushing ${IMG}"
docker push ${IMG}
docker push "${IMG}-app"
docker push "${IMG}-worker"

14
bash/build_base.sh Executable file
View File

@@ -0,0 +1,14 @@
# torsearch-app is built from this base
export APP_FRANKENPHP_TAG=php8.4
docker buildx build --platform=linux/amd64 -f docker/Dockerfile.base.app -t code.caldwell.digital/home/torsearch-base:${APP_FRANKENPHP_TAG} -t code.caldwell.digital/home/torsearch-base:latest --build-arg "FRANKENPHP_TAG=${APP_FRANKENPHP_TAG}" .
docker push code.caldwell.digital/home/torsearch-base:${APP_FRANKENPHP_TAG}
docker push code.caldwell.digital/home/torsearch-base:latest
# torsearch-worker & torsearch-scheduler are built from this base
export WORKER_FRANKENPHP_TAG=php8.4-alpine
docker buildx build --platform=linux/amd64 -f docker/Dockerfile.base.worker -t code.caldwell.digital/home/torsearch-base-worker:${WORKER_FRANKENPHP_TAG} -t code.caldwell.digital/home/torsearch-base-worker:latest --build-arg "FRANKENPHP_TAG=${WORKER_FRANKENPHP_TAG}" .
docker push code.caldwell.digital/home/torsearch-base-worker:${WORKER_FRANKENPHP_TAG}
docker push code.caldwell.digital/home/torsearch-base-worker:latest

View File

@@ -1,6 +1,11 @@
dev.caldwell.digital:443
{
log {
level DEBUG
}
}
tls /etc/ssl/wildcard.crt /etc/ssl/wildcard.pem
reverse_proxy php:80
dev.caldwell.digital:443 {
tls /etc/ssl/wildcard.crt /etc/ssl/wildcard.pem
reverse_proxy app:80
}

View File

@@ -1,18 +0,0 @@
<VirtualHost *:80>
ServerName localhost
DocumentRoot /var/www/public
DirectoryIndex /index.php
<Directory /var/www/public>
AllowOverride None
Order Allow,Deny
Allow from All
FallbackResource /index.php
</Directory>
<Directory /var/www/public/bundles>
FallbackResource disabled
</Directory>
</VirtualHost>

23
bin/phpunit Executable file
View File

@@ -0,0 +1,23 @@
#!/usr/bin/env php
<?php
if (!ini_get('date.timezone')) {
ini_set('date.timezone', 'UTC');
}
if (is_file(dirname(__DIR__).'/vendor/phpunit/phpunit/phpunit')) {
if (PHP_VERSION_ID >= 80000) {
require dirname(__DIR__).'/vendor/phpunit/phpunit/phpunit';
} else {
define('PHPUNIT_COMPOSER_INSTALL', dirname(__DIR__).'/vendor/autoload.php');
require PHPUNIT_COMPOSER_INSTALL;
PHPUnit\TextUI\Command::main();
}
} else {
if (!is_file(dirname(__DIR__).'/vendor/symfony/phpunit-bridge/bin/simple-phpunit.php')) {
echo "Unable to find the `simple-phpunit.php` script in `vendor/symfony/phpunit-bridge/bin/`.\n";
exit(1);
}
require dirname(__DIR__).'/vendor/symfony/phpunit-bridge/bin/simple-phpunit.php';
}

View File

@@ -1,28 +1,44 @@
<?xml version="1.0" encoding="UTF-8"?>
<project name="Caldwell Digital Symfony Template" default="build">
<project name="Torsearch" default="build">
<!-- build dev for dev envs -->
<target name="build" depends="setEnv,composer,compileAssets" />
<target name="build" depends="setEnv,composer,compileAssets,migrateDb,clearCache" />
<target name="composer" description="Run composer">
<exec executable="composer">
<arg value="install" />
<arg value="--ignore-platform-reqs" />
</exec>
</target>
<target name="setEnv" description="Set the database configuration">
<copy file="${project.basedir}/.env.properties" tofile="${project.basedir}/.env.local" overwrite="true">
</copy>
</target>
<target name="compileAssets" description="Run composer">
<exec executable="php">
<arg value="bin/console" />
<arg value="tailwind:build" />
</exec>
<exec executable="php">
<arg value="bin/console" />
<arg value="asset-map:compile" />
</exec>
</target>
<target name="setEnv" description="Set the database configuration">
<copy file="${project.basedir}/.env.dist" tofile="${project.basedir}/.env.local" overwrite="true">
<filterchain>
<replacetokens begintoken="%%" endtoken="%%">
<token key="db_url" value="${DATABASE_URL}" />
</replacetokens>
</filterchain>
</copy>
<target name="migrateDb" description="Migrate the database">
<exec executable="php">
<arg value="bin/console" />
<arg value="--no-interaction" />
<arg value="doctrine:migrations:migrate" />
</exec>
</target>
<target name="clearCache" description="Clear the application cache">
<exec executable="php">
<arg value="bin/console" />
<arg value="cache:pool:clear" />
<arg value="cache.app" />
</exec>
</target>
</project>

View File

@@ -2,6 +2,7 @@ services:
caddy:
image: caddy:2.9.1
restart: unless-stopped
tty: true
cap_add:
- NET_ADMIN
ports:
@@ -12,10 +13,65 @@ services:
- $PWD/bash/caddy:/etc/caddy
- $PWD/bash/certs:/etc/ssl
php:
app:
build: .
restart: unless-stopped
volumes:
- ./:/var/www
- $PWD:/app
- $PWD/var/download:/var/download
- mercure_data:/data
- mercure_config:/config
tty: true
environment:
TZ: America/Chicago
MERCURE_PUBLISHER_JWT_KEY: '!ChangeThisMercureHubJWTSecretKey!'
MERCURE_SUBSCRIBER_JWT_KEY: '!ChangeThisMercureHubJWTSecretKey!'
depends_on:
database:
condition: service_healthy
worker:
build:
dockerfile: docker/Dockerfile.base.worker
context: .
args:
FRANKENPHP_TAG: php8.4-alpine
restart: unless-stopped
volumes:
- $PWD:/app
- $PWD/var/download:/var/download
tty: true
environment:
TZ: America/Chicago
command: php /app/bin/console messenger:consume async --time-limit=3600 -vv
scheduler:
build:
dockerfile: docker/Dockerfile.base.worker
context: .
args:
FRANKENPHP_TAG: php8.4-alpine
restart: unless-stopped
volumes:
- $PWD:/app
environment:
TZ: America/Chicago
command: php /app/bin/console messenger:consume scheduler_monitor -vv
tty: true
redis:
image: redis:latest
volumes:
- redis_data:/data
command: redis-server --maxmemory 512MB
restart: unless-stopped
environment:
TZ: America/Chicago
database:
image: mariadb:10.11.2
@@ -24,15 +80,26 @@ services:
volumes:
- mysql:/var/lib/mysql
environment:
TZ: America/Chicago
MYSQL_DATABASE: app
MYSQL_USERNAME: app
MYSQL_PASSWORD: password
MYSQL_ROOT_PASSWORD: password
healthcheck:
test: [ "CMD", "mysqladmin" ,"ping", "-h", "localhost" ]
interval: 5s
timeout: 5s
retries: 10
adminer:
image: adminer
ports:
- "8081:8080"
volumes:
mysql:
mercure_data:
mercure_config:
redis_data:

View File

@@ -4,29 +4,70 @@
"minimum-stability": "stable",
"prefer-stable": true,
"require": {
"php": ">=8.2",
"php": ">=8.4",
"ext-ctype": "*",
"ext-iconv": "*",
"1tomany/rich-bundle": "^1.8",
"aimeos/map": "^3.12",
"chrisullyott/php-filesize": "^4.2",
"doctrine/dbal": "^3",
"doctrine/doctrine-bundle": "^2.14",
"doctrine/doctrine-fixtures-bundle": "^4.1",
"doctrine/doctrine-migrations-bundle": "^3.4",
"doctrine/orm": "^3.3",
"dragonmantank/cron-expression": "^3.4",
"drenso/symfony-oidc-bundle": "^4.2",
"guzzlehttp/guzzle": "^7.9",
"league/pipeline": "^1.1",
"nesbot/carbon": "^3.9",
"nihilarr/parse-torrent-name": "^0.0.1",
"nyholm/psr7": "*",
"p3k/emoji-detector": "^1.2",
"php-http/cache-plugin": "^2.0",
"php-tmdb/api": "^4.1",
"symfony/asset": "7.2.*",
"symfony/console": "7.2.*",
"symfony/dotenv": "7.2.*",
"phpdocumentor/reflection-docblock": "^5.6",
"phpstan/phpdoc-parser": "^2.1",
"predis/predis": "^2.4",
"runtime/frankenphp-symfony": "^0.2.0",
"spatie/icalendar-generator": "^3.0",
"spomky-labs/pwa-bundle": "^1.2",
"stof/doctrine-extensions-bundle": "^1.14",
"symfony/asset": "7.3.*",
"symfony/console": "7.3.*",
"symfony/doctrine-messenger": "7.3.*",
"symfony/dotenv": "7.3.*",
"symfony/filesystem": "7.3.*",
"symfony/finder": "7.3.*",
"symfony/flex": "^2",
"symfony/framework-bundle": "7.2.*",
"symfony/runtime": "7.2.*",
"symfony/security-bundle": "7.2.*",
"symfony/form": "7.3.*",
"symfony/framework-bundle": "7.3.*",
"symfony/http-client": "7.3.*",
"symfony/ldap": "7.3.*",
"symfony/mailer": "7.3.*",
"symfony/mercure-bundle": "^0.3.9",
"symfony/messenger": "7.3.*",
"symfony/notifier": "7.3.*",
"symfony/ntfy-notifier": "7.3.*",
"symfony/object-mapper": "7.3.*",
"symfony/property-access": "7.3.*",
"symfony/property-info": "7.3.*",
"symfony/runtime": "7.3.*",
"symfony/scheduler": "7.3.*",
"symfony/security-bundle": "7.3.*",
"symfony/serializer": "7.3.*",
"symfony/stimulus-bundle": "^2.24",
"symfony/twig-bundle": "7.2.*",
"symfony/twig-bundle": "7.3.*",
"symfony/ux-autocomplete": "^2.27",
"symfony/ux-icons": "^2.24",
"symfony/ux-live-component": "^2.24",
"symfony/ux-turbo": "^2.24",
"symfony/ux-twig-component": "^2.24",
"symfony/yaml": "7.2.*",
"symfony/yaml": "7.3.*",
"symfonycasts/reset-password-bundle": "^1.23",
"symfonycasts/tailwind-bundle": "^0.10.0",
"twig/extra-bundle": "^2.12|^3.0",
"twig/twig": "^2.12|^3.0"
"twig/twig": "^2.12|^3.0",
"web-token/jwt-library": "^4.0"
},
"config": {
"allow-plugins": {
@@ -34,6 +75,9 @@
"symfony/flex": true,
"symfony/runtime": true
},
"platform": {
"php": "8.4"
},
"bump-after-update": true,
"sort-packages": true
},
@@ -69,7 +113,8 @@
"post-update-cmd": [
"@auto-scripts"
],
"sym": "docker compose exec php ./bin/console"
"tail": "docker compose exec app ./bin/console tailwind:build --watch",
"sym": "docker compose exec app ./bin/console"
},
"conflict": {
"symfony/symfony": "*"
@@ -77,10 +122,14 @@
"extra": {
"symfony": {
"allow-contrib": false,
"require": "7.2.*"
"require": "7.3.*"
}
},
"require-dev": {
"symfony/maker-bundle": "^1.62"
"phpstan/phpstan": "^2.1",
"phpunit/phpunit": "^12.3",
"symfony/maker-bundle": "^1.62",
"symfony/stopwatch": "7.3.*",
"symfony/web-profiler-bundle": "7.3.*"
}
}

8226
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -11,4 +11,16 @@ return [
OneToMany\RichBundle\RichBundle::class => ['all' => true],
Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true],
Symfony\UX\StimulusBundle\StimulusBundle::class => ['all' => true],
Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true],
Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true],
Symfony\UX\LiveComponent\LiveComponentBundle::class => ['all' => true],
Symfony\Bundle\MercureBundle\MercureBundle::class => ['all' => true],
Symfony\UX\Turbo\TurboBundle::class => ['all' => true],
Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true],
Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle::class => ['dev' => true, 'test' => true],
Stof\DoctrineExtensionsBundle\StofDoctrineExtensionsBundle::class => ['all' => true],
Symfony\UX\Autocomplete\AutocompleteBundle::class => ['all' => true],
SymfonyCasts\Bundle\ResetPassword\SymfonyCastsResetPasswordBundle::class => ['all' => true],
Drenso\OidcBundle\DrensoOidcBundle::class => ['all' => true],
SpomkyLabs\PwaBundle\SpomkyLabsPwaBundle::class => ['all' => true],
];

57
config/dist/ldap.security.yaml vendored Normal file
View File

@@ -0,0 +1,57 @@
security:
# https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords
password_hashers:
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
# https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider
providers:
users_in_memory: { memory: null }
app_local:
entity:
class: App\User\Framework\Entity\User
property: email
app_ldap:
id: App\User\Framework\Security\LdapUserProvider
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
lazy: true
provider: app_ldap
form_login_ldap:
login_path: app_login
check_path: app_login
enable_csrf: true
service: Symfony\Component\Ldap\Ldap
dn_string: '%env(LDAP_DN_STRING)%'
logout:
path: app_logout
# activate different ways to authenticate
# https://symfony.com/doc/current/security.html#the-firewall
# https://symfony.com/doc/current/security/impersonating_user.html
# switch_user: true
# Easy way to control access for large sections of your site
# Note: Only the *first* access control that matches will be used
access_control:
- { path: ^/getting-started, roles: PUBLIC_ACCESS }
- { path: ^/register, roles: PUBLIC_ACCESS }
- { path: ^/login, roles: PUBLIC_ACCESS }
- { path: ^/, roles: ROLE_USER } # Or ROLE_ADMIN, ROLE_SUPER_ADMIN,
when@test:
security:
password_hashers:
# By default, password hashers are resource intensive and take time. This is
# important to generate secure password hashes. In tests however, secure hashes
# are not important, waste resources and increase test times. The following
# reduces the work factor to the lowest possible values.
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface:
algorithm: auto
cost: 4 # Lowest possible value for bcrypt
time_cost: 3 # Lowest possible value for argon
memory_cost: 10 # Lowest possible value for argon

55
config/dist/local.security.yaml vendored Normal file
View File

@@ -0,0 +1,55 @@
security:
# https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords
password_hashers:
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
# https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider
providers:
users_in_memory: { memory: null }
app_local:
entity:
class: App\User\Framework\Entity\User
property: email
app_ldap:
id: App\User\Framework\Security\LdapUserProvider
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
lazy: true
provider: app_local
form_login:
login_path: app_login
check_path: app_login
enable_csrf: true
logout:
path: app_logout
# activate different ways to authenticate
# https://symfony.com/doc/current/security.html#the-firewall
# https://symfony.com/doc/current/security/impersonating_user.html
# switch_user: true
# Easy way to control access for large sections of your site
# Note: Only the *first* access control that matches will be used
access_control:
- { path: ^/getting-started, roles: PUBLIC_ACCESS }
- { path: ^/register, roles: PUBLIC_ACCESS }
- { path: ^/login, roles: PUBLIC_ACCESS }
- { path: ^/, roles: ROLE_USER } # Or ROLE_ADMIN, ROLE_SUPER_ADMIN,
when@test:
security:
password_hashers:
# By default, password hashers are resource intensive and take time. This is
# important to generate secure password hashes. In tests however, secure hashes
# are not important, waste resources and increase test times. The following
# reduces the work factor to the lowest possible values.
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface:
algorithm: auto
cost: 4 # Lowest possible value for bcrypt
time_cost: 3 # Lowest possible value for argon
memory_cost: 10 # Lowest possible value for argon

View File

@@ -8,8 +8,8 @@ framework:
# Other options include:
# Redis
#app: cache.adapter.redis
#default_redis_provider: redis://localhost
app: cache.adapter.redis
default_redis_provider: '%app.cache.redis.host%'
# APCu (not recommended with heavy random-write workloads as memory fragmentation can cause perf issues)
#app: cache.adapter.apcu

11
config/packages/csrf.yaml Normal file
View File

@@ -0,0 +1,11 @@
# Enable stateless CSRF protection for forms and logins/logouts
framework:
form:
csrf_protection:
token_id: submit
csrf_protection:
stateless_token_ids:
- submit
- authenticate
- logout

View File

@@ -0,0 +1,78 @@
doctrine:
dbal:
url: '%env(resolve:DATABASE_URL)%'
# IMPORTANT: You MUST configure your server version,
# either here or in the DATABASE_URL env var (see .env file)
#server_version: '16'
profiling_collect_backtrace: '%kernel.debug%'
use_savepoints: true
orm:
auto_generate_proxy_classes: true
enable_lazy_ghost_objects: true
report_fields_where_declared: true
validate_xml_mapping: true
naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware
identity_generation_preferences:
Doctrine\DBAL\Platforms\PostgreSQLPlatform: identity
auto_mapping: true
mappings:
# App:
# type: attribute
# is_bundle: false
# dir: '%kernel.project_dir%/src/Entity'
# prefix: 'App\Entity'
# alias: App
Download:
type: attribute
is_bundle: false
dir: '%kernel.project_dir%/src/Download/Framework/Entity'
prefix: 'App\Download\Framework\Entity'
alias: Download
User:
type: attribute
is_bundle: false
dir: '%kernel.project_dir%/src/User/Framework/Entity'
prefix: 'App\User\Framework\Entity'
alias: User
Monitor:
type: attribute
is_bundle: false
dir: '%kernel.project_dir%/src/Monitor/Framework/Entity'
prefix: 'App\Monitor\Framework\Entity'
alias: Monitor
EventLog:
type: attribute
is_bundle: false
dir: '%kernel.project_dir%/src/EventLog/Framework/Entity'
prefix: 'App\EventLog\Framework\Entity'
alias: EventLog
controller_resolver:
auto_mapping: false
when@test:
doctrine:
dbal:
# "TEST_TOKEN" is typically set by ParaTest
dbname_suffix: '_test%env(default::TEST_TOKEN)%'
when@prod:
doctrine:
orm:
auto_generate_proxy_classes: false
proxy_dir: '%kernel.build_dir%/doctrine/orm/Proxies'
query_cache_driver:
type: pool
pool: doctrine.system_cache_pool
result_cache_driver:
type: pool
pool: doctrine.result_cache_pool
framework:
cache:
pools:
doctrine.result_cache_pool:
adapter: cache.app
doctrine.system_cache_pool:
adapter: cache.system

View File

@@ -0,0 +1,6 @@
doctrine_migrations:
migrations_paths:
# namespace is arbitrary but should be different from App\Migrations
# as migrations classes should NOT be autoloaded
'DoctrineMigrations': '%kernel.project_dir%/migrations'
enable_profiler: false

View File

@@ -0,0 +1,19 @@
drenso_oidc:
#default_client: default # The default client, will be aliased to OidcClientInterface
clients:
default: # The client name, each client will be aliased to its name (for example, $defaultOidcClient)
# Required OIDC client configuration
well_known_url: '%env(OIDC_WELL_KNOWN_URL)%'
client_id: '%env(OIDC_CLIENT_ID)%'
client_secret: '%env(OIDC_CLIENT_SECRET)%'
redirect_route: '/login/oidc/auth'
# Extra configuration options
#redirect_route: '/login_check'
#custom_client_headers: []
# Add any extra client
#link: # Will be accessible using $linkOidcClient
#well_known_url: '%env(LINK_WELL_KNOWN_URL)%'
#client_id: '%env(LINK_CLIENT_ID)%'
#client_secret: '%env(LINK_CLIENT_SECRET)%'

View File

@@ -2,8 +2,16 @@
framework:
secret: '%env(APP_SECRET)%'
# Note that the session will be started ONLY if you read or write from it.
session: true
serializer:
default_context:
enable_max_depth: true
trusted_proxies: 'private_ranges'
# trust *all* "X-Forwarded-*" headers
trusted_headers: [ 'x-forwarded-for', 'x-forwarded-host', 'x-forwarded-proto', 'x-forwarded-port', 'x-forwarded-prefix' ]
session:
handler_id: '%env(REDIS_HOST)%'
#esi: true
#fragments: true

View File

@@ -0,0 +1,7 @@
framework:
mailer:
dsn: 'smtp://%env(SMTP_USER)%:%env(SMTP_PASS)%@%env(SMTP_HOST)%:%env(SMTP_PORT)%'
envelope:
sender: '%env(SMTP_FROM)%'
headers:
From: '%env(SMTP_FROM_NAME)% <%env(SMTP_FROM)%>'

View File

@@ -0,0 +1,8 @@
mercure:
hubs:
default:
url: '%app.mercure.url%'
public_url: '%app.mercure.public_url%'
jwt:
secret: '%env(MERCURE_JWT_SECRET)%'
publish: '*'

View File

@@ -0,0 +1,53 @@
framework:
messenger:
# Uncomment this (and the failed transport below) to send failed messages to this transport for later handling.
failure_transport: failed
transports:
# https://symfony.com/doc/current/messenger.html#transport-configuration
async:
dsn: '%env(MESSENGER_TRANSPORT_DSN)%'
options:
use_notify: true
check_delayed_interval: 60000
queue_name: default
retry_strategy:
max_retries: 1
multiplier: 1
media_cache:
dsn: '%env(MESSENGER_TRANSPORT_DSN)%'
options:
use_notify: true
check_delayed_interval: 60000
queue_name: media_cache
retry_strategy:
max_retries: 1
multiplier: 1
failed: 'doctrine://default?queue_name=failed'
default_bus: messenger.bus.default
buses:
messenger.bus.default: []
routing:
# Route your messages to the transports
# 'App\Message\YourMessage': async
'App\Download\Action\Command\DownloadMediaCommand': async
'App\Download\Action\Command\DownloadSeasonCommand': async
'App\Monitor\Action\Command\MonitorTvEpisodeCommand': async
'App\Monitor\Action\Command\MonitorTvSeasonCommand': async
'App\Monitor\Action\Command\MonitorTvShowCommand': async
'App\Monitor\Action\Command\MonitorMovieCommand': async
'App\Torrentio\Action\Command\GetTvShowOptionsCommand': media_cache
# when@test:
# framework:
# messenger:
# transports:
# # replace with your transport name here (e.g., my_transport: 'in-memory://')
# # For more Messenger testing tools, see https://github.com/zenstruck/messenger-test
# async: 'in-memory://'

View File

@@ -0,0 +1,13 @@
framework:
notifier:
chatter_transports:
texter_transports:
ntfy: '%notification.ntfy.dsn%'
channel_policy:
# use chat/slack, chat/telegram, sms/twilio or sms/nexmo
urgent: ['email']
high: ['email']
medium: ['email']
low: ['email']
admin_recipients:
- { email: admin@example.com }

View File

@@ -0,0 +1,3 @@
framework:
property_info:
with_constructor_extractor: true

21
config/packages/pwa.yaml Normal file
View File

@@ -0,0 +1,21 @@
pwa:
manifest:
enabled: true
name: "Torsearch"
short_name: "torsearch"
start_url: "/"
display: "standalone"
id: "/"
background_color: "#f98e44"
theme_color: "#083344"
description: Torsearch provides a simple and intuitive way to manage your personal media library.
icons:
- src: "/icon.png"
sizes: [ 192 ]
- src: "/icon.png"
sizes: [ 192 ]
purpose: maskable
categories:
- entertainment
- multimedia
- utilities

View File

@@ -0,0 +1,2 @@
symfonycasts_reset_password:
request_password_repository: App\User\Framework\Repository\ResetPasswordRequestRepository

View File

@@ -5,13 +5,36 @@ security:
# https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider
providers:
users_in_memory: { memory: null }
app_local:
entity:
class: App\User\Framework\Entity\User
property: email
app_oidc:
id: App\User\Framework\Security\OidcUserProvider
app_ldap:
id: App\User\Framework\Security\LdapUserProvider
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
lazy: true
provider: users_in_memory
logout:
path: /logout
provider: app_oidc
form_login:
login_path: app_login
check_path: app_login
enable_csrf: true
oidc:
login_path: '/login/oidc'
check_path: '/login/oidc/auth'
enable_end_session_listener: true
http_basic:
realm: Secured Area
entry_point: form_login
# activate different ways to authenticate
# https://symfony.com/doc/current/security.html#the-firewall
@@ -22,8 +45,12 @@ security:
# Easy way to control access for large sections of your site
# Note: Only the *first* access control that matches will be used
access_control:
# - { path: ^/admin, roles: ROLE_ADMIN }
# - { path: ^/profile, roles: ROLE_USER }
- { path: ^/monitors/ical/, roles: PUBLIC_ACCESS }
- { path: ^/reset-password, roles: PUBLIC_ACCESS }
- { path: ^/getting-started, roles: PUBLIC_ACCESS }
- { path: ^/register, roles: PUBLIC_ACCESS }
- { path: ^/login, roles: PUBLIC_ACCESS }
- { path: ^/, roles: ROLE_USER } # Or ROLE_ADMIN, ROLE_SUPER_ADMIN,
when@test:
security:

View File

@@ -0,0 +1,7 @@
# Read the documentation: https://symfony.com/doc/current/bundles/StofDoctrineExtensionsBundle/index.html
# See the official DoctrineExtensions documentation for more details: https://github.com/doctrine-extensions/DoctrineExtensions/tree/main/doc
stof_doctrine_extensions:
default_locale: en_US
orm:
default:
timestampable: true

View File

@@ -0,0 +1,7 @@
framework:
default_locale: en
translator:
default_path: '%kernel.project_dir%/translations'
fallbacks:
- en
providers:

View File

@@ -1,5 +1,10 @@
twig:
globals:
version: '%app.version%'
file_name_pattern: '*.twig'
date:
format: 'm/d/Y'
timezone: '%env(default:app.default.timezone:TZ)%'
when@test:
twig:

View File

@@ -0,0 +1,11 @@
when@dev:
web_profiler:
toolbar: true
framework:
profiler:
collect_serializer_data: true
when@test:
framework:
profiler: { collect: false }

View File

@@ -1,8 +1,71 @@
controllersIndex:
controllersBase:
resource:
path: ../src/Controller/
namespace: App\Controller
path: ../src/Base/Framework/Controller/
namespace: App\Base\Framework\Controller
type: attribute
defaults:
schemes: [ https ]
schemes: [ 'https' ]
controllersEventLog:
resource:
path: ../src/EventLog/Framework/Controller/
namespace: App\EventLog\Framework\Controller
type: attribute
defaults:
schemes: [ 'https' ]
controllersLibrary:
resource:
path: ../src/Library/Framework/Controller/
namespace: App\Library\Framework\Controller
type: attribute
defaults:
schemes: [ 'https' ]
controllersSearch:
resource:
path: ../src/Search/Framework/Controller/
namespace: App\Search\Framework\Controller
type: attribute
defaults:
schemes: [ 'https' ]
controllersUser:
resource:
path: ../src/User/Framework/Controller
namespace: App\User\Framework\Controller
type: attribute
defaults:
schemes: ['https']
controllersDownload:
resource:
path: ../src/Download/Framework/Controller
namespace: App\Download\Framework\Controller
type: attribute
defaults:
schemes: [ 'https' ]
controllersMonitor:
resource:
path: ../src/Monitor/Framework/Controller
namespace: App\Monitor\Framework\Controller
type: attribute
defaults:
schemes: ['https']
controllersTorrentio:
resource:
path: ../src/Torrentio/Framework/Controller
namespace: App\Torrentio\Framework\Controller
type: attribute
defaults:
schemes: ['https']
controllersTmdb:
resource:
path: ../src/Tmdb/Framework/Controller
namespace: App\Tmdb\Framework\Controller
type: attribute
defaults:
schemes: ['https']

View File

@@ -0,0 +1,3 @@
ux_autocomplete:
resource: '@AutocompleteBundle/config/routes.php'
prefix: '/autocomplete'

View File

@@ -0,0 +1,5 @@
live_component:
resource: '@LiveComponentBundle/config/routes.php'
prefix: '/_components'
# adjust prefix to add localization to your components
#prefix: '/{_locale}/_components'

View File

@@ -0,0 +1,8 @@
when@dev:
web_profiler_wdt:
resource: '@WebProfilerBundle/Resources/config/routing/wdt.xml'
prefix: /_wdt
web_profiler_profiler:
resource: '@WebProfilerBundle/Resources/config/routing/profiler.xml'
prefix: /_profiler

View File

@@ -4,6 +4,49 @@
# Put parameters here that don't need to change on each machine where the app is deployed
# https://symfony.com/doc/current/best_practices.html#use-parameters-for-application-configuration
parameters:
# App
app.url: '%env(APP_URL)%'
app.version: '%env(default:app.default.version:APP_VERSION)%'
# Debrid Services
app.debrid.real_debrid.key: '%env(REAL_DEBRID_KEY)%'
# TMDB Key
app.meta_provider.tmdb.key: '%env(TMDB_API)%'
# Media
media.base_path: '/var/download'
media.default_movies_dir: movies
media.default_tvshows_dir: tvshows
media.movies_path: '/var/download/%env(default:media.default_movies_dir:MOVIES_PATH)%'
media.tvshows_path: '/var/download/%env(default:media.default_tvshows_dir:TVSHOWS_PATH)%'
# Mercure
app.mercure.url: 'http://app/.well-known/mercure'
app.mercure.public_url: '%env(APP_URL)%/.well-known/mercure'
# Cache
app.cache.adapter: '%env(default:app.cache.adapter.default:CACHE_ADAPTER)%'
app.cache.redis.host: '%env(default:app.cache.redis.host.default:REDIS_HOST)%'
app.cache.adapter.default: 'filesystem'
app.cache.redis.host.default: 'redis://redis'
# Various configs
app.default.version: '0.0.0-dev'
app.default.timezone: 'America/Chicago'
# Auth
auth.default.method: 'form_login'
auth.method: '%env(default:auth.default.method:AUTH_METHOD)%'
auth.oidc.well_known_url: '%env(OIDC_WELL_KNOWN_URL)%'
auth.oidc.client_id: '%env(OIDC_CLIENT_ID)%'
auth.oidc.client_secret: '%env(OIDC_CLIENT_SECRET)%'
auth.oidc.bypass_form_login: '%env(bool:OIDC_BYPASS_FORM_LOGIN)%'
# Notifications
notification.transport: '%env(NOTIFICATION_TRANSPORT)%'
notification.ntfy.dsn: '%env(NTFY_DSN)%'
services:
# default configuration for services in *this* file
@@ -22,3 +65,40 @@ services:
# add more service definitions when explicit configuration is needed
# please note that last definitions always *replace* previous ones
App\Download\Downloader\DownloaderInterface: "@App\\Download\\Downloader\\ProcessDownloader"
# Session
Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler:
arguments:
- '%env(DATABASE_URL)%'
# LDAP
App\User\Framework\Security\LdapUserProvider:
arguments:
$registerLdapUserHandler: '@App\User\Action\Handler\RegisterLdapUserHandler'
$userRepository: '@App\User\Framework\Repository\UserRepository'
$ldap: '@Symfony\Component\Ldap\LdapInterface'
$baseDn: '%env(LDAP_BASE_DN)%'
$searchDn: '%env(LDAP_BIND_USER)%'
$searchPassword: '%env(LDAP_BIND_PASS)%'
$defaultRoles: ['ROLE_USER']
$uidKey: '%env(LDAP_UID_KEY)%'
# $passwordAttribute: '%env(LDAP_PASSWORD_ATTRIBUTE)%'
Symfony\Component\Ldap\LdapInterface: '@Symfony\Component\Ldap\Ldap'
Symfony\Component\Ldap\Ldap:
arguments: [ '@Symfony\Component\Ldap\Adapter\ExtLdap\Adapter' ]
tags:
- ldap
Symfony\Component\Ldap\Adapter\ExtLdap\Adapter:
arguments:
- host: '%env(LDAP_HOST)%'
port: '%env(LDAP_PORT)%'
encryption: '%env(LDAP_ENCRYPTION)%'
options:
protocol_version: 3
referrals: false

View File

@@ -1,5 +1,57 @@
services:
php:
image: code.caldwell.digital/home/torsearch/app:${TAG}
app:
image: registry.caldwell.digital/home/torsearch-app:${TAG}
ports:
- "8001:80"
- "${SWARM_PORT}:80"
environment:
MERCURE_PUBLISHER_JWT_KEY: '!ChangeThisMercureHubJWTSecretKey!'
MERCURE_SUBSCRIBER_JWT_KEY: '!ChangeThisMercureHubJWTSecretKey!'
deploy:
replicas: 2
volumes:
- /mnt/media/downloads/movies:/var/download/movies
- /mnt/media/downloads/tvshows:/var/download/tvshows
- mercure_data:/data
- mercure_config:/config
depends_on:
- database
worker:
image: registry.caldwell.digital/home/torsearch-worker:${TAG}
volumes:
- /mnt/media/downloads/movies:/var/download/movies
- /mnt/media/downloads/tvshows:/var/download/tvshows
restart: always
command: -vv --time-limit=3600 --limit=10
deploy:
replicas: 2
depends_on:
- app
scheduler:
image: registry.caldwell.digital/home/torsearch-scheduler:${TAG}
volumes:
- /mnt/media/downloads/movies:/var/download/movies
- /mnt/media/downloads/tvshows:/var/download/tvshows
restart: always
command: -vv
depends_on:
- app
redis:
image: redis:latest
volumes:
- redis_data:/data
command: redis-server --maxmemory 512MB
restart: unless-stopped
volumes:
mysql:
mercure_config:
mercure_data:
redis_data:

View File

@@ -2,6 +2,7 @@ assets
bash
bin
config
docker
migrations
public
src
@@ -9,6 +10,7 @@ templates
var
vendor
build.xml
.git
.env
.env.local
composer.json

14
docker/Dockerfile.app Normal file
View File

@@ -0,0 +1,14 @@
FROM code.caldwell.digital/home/torsearch-base:php8.4
ARG APP_VERSION="0.0.0-dev"
ENV APP_VERSION="${APP_VERSION}"
COPY . /app
COPY --chmod=775 docker/app/entrypoint.sh /usr/local/bin/docker-entrypoint
COPY docker/app/Caddyfile /etc/frankenphp/Caddyfile
ENTRYPOINT [ "/usr/local/bin/docker-entrypoint" ]
CMD [ "frankenphp", "run", "--config", "/etc/frankenphp/Caddyfile" ]
HEALTHCHECK --interval=3s --timeout=3s --retries=10 CMD [ "php", "/app/bin/console", "startup:status" ]

View File

@@ -0,0 +1,14 @@
ARG FRANKENPHP_TAG
FROM dunglas/frankenphp:${FRANKENPHP_TAG}
ENV SERVER_NAME=":80"
ENV CADDY_GLOBAL_OPTIONS="auto_https off"
ENV APP_RUNTIME="Runtime\\FrankenPhpSymfony\\Runtime"
RUN install-php-extensions \
pdo_mysql \
gd \
intl \
zip \
opcache

View File

@@ -0,0 +1,19 @@
ARG FRANKENPHP_TAG
FROM dunglas/frankenphp:${FRANKENPHP_TAG}
ENV SERVER_NAME=":80"
ENV CADDY_GLOBAL_OPTIONS="auto_https off"
ENV APP_RUNTIME="Runtime\\FrankenPhpSymfony\\Runtime"
ARG APP_VERSION="0.dev"
ENV APP_VERSION="${APP_VERSION}"
RUN install-php-extensions \
pdo_mysql \
gd \
intl \
zip \
opcache
RUN apk add --no-cache wget

View File

@@ -0,0 +1,10 @@
FROM code.caldwell.digital/home/torsearch-base-worker:php8.4-alpine
ARG APP_VERSION="0.0.0-dev"
ENV APP_VERSION="${APP_VERSION}"
COPY . /app
ENTRYPOINT [ "php", "/app/bin/console", "messenger:consume", "scheduler_monitor" ]
HEALTHCHECK --interval=3s --timeout=3s --retries=10 CMD return 0

10
docker/Dockerfile.worker Normal file
View File

@@ -0,0 +1,10 @@
FROM code.caldwell.digital/home/torsearch-base-worker:php8.4-alpine
ARG APP_VERSION="0.0.0-dev"
ENV APP_VERSION="${APP_VERSION}"
COPY . /app
ENTRYPOINT [ "php", "/app/bin/console", "messenger:consume", "async" ]
HEALTHCHECK --interval=3s --timeout=3s --retries=10 CMD return 0

59
docker/app/Caddyfile Normal file
View File

@@ -0,0 +1,59 @@
{
{$CADDY_GLOBAL_OPTIONS}
frankenphp {
{$FRANKENPHP_CONFIG}
}
}
{$CADDY_EXTRA_CONFIG}
{$SERVER_NAME:localhost} {
log {
{$CADDY_SERVER_LOG_OPTIONS}
# Redact the authorization query parameter that can be set by Mercure
format filter {
request>uri query {
replace authorization REDACTED
}
}
}
root /app/public
encode zstd br gzip
mercure {
# Publisher JWT key
publisher_jwt {env.MERCURE_PUBLISHER_JWT_KEY} {env.MERCURE_PUBLISHER_JWT_ALG}
# Subscriber JWT key
subscriber_jwt {env.MERCURE_SUBSCRIBER_JWT_KEY} {env.MERCURE_SUBSCRIBER_JWT_ALG}
# Allow anonymous subscribers (double-check that it's what you want)
anonymous
# Enable the subscription API (double-check that it's what you want)
subscriptions
# Custmo cors
cors_origins *
# Extra directives
{$MERCURE_EXTRA_DIRECTIVES}
}
vulcain
{$CADDY_SERVER_EXTRA_DIRECTIVES}
# Disable Topics tracking if not enabled explicitly: https://github.com/jkarlin/topics
header ?Permissions-Policy "browsing-topics=()"
@phpRoute {
not path /.well-known/mercure*
not file {path}
}
rewrite @phpRoute index.php
@frontController path index.php
php @frontController
file_server {
hide *.php
}
}

12
docker/app/entrypoint.sh Normal file
View File

@@ -0,0 +1,12 @@
#!/bin/sh
# Sleep for a second to ensure DB is awake and ready
SLEEP_TIME=$(shuf -i 2-4 -n 1)
echo "> Sleeping for ${SLEEP_TIME} seconds to wait for the database"
sleep $SLEEP_TIME
# Provision database
php /app/bin/console doctrine:migrations:migrate --no-interaction
php /app/bin/console db:seed
exec docker-php-entrypoint "$@"

View File

@@ -0,0 +1,12 @@
[Unit]
Description=Torsearch media cache warming services
[Service]
ExecStart=php /app/bin/console messenger:consume media_cache --time-limit=3600
# for Redis, set a custom consumer name for each instance
Environment="MESSENGER_CONSUMER_NAME=symfony-%n-%i"
Restart=always
RestartSec=30
[Install]
WantedBy=default.target

71
docs/examples/.env Normal file
View File

@@ -0,0 +1,71 @@
# App must be served over HTTPS (requirement of Mercure)
# Either serve behind an SSL terminating reverse proxy
# or pass your certificates into the 'app' container.
# Please omit any trailing slashes. The APP_URL is
# used to generate the Mercure URL behind the scenes.
APP_URL="https://dev.caldwell.digital"
APP_SECRET="70169beadfbc8101c393cbfbba27a313"
APP_ENV=prod
# Mercure is a Caddy module built into the webserver
# that facilitates the usage of websockets to transmit
# real time data (download progress, etc.)
MERCURE_JWT_SECRET="!ChangeThisMercureHubJWTSecretKey!"
# Use the DATABASE_URL below to use the MariaDB container
# provided in the example.compose.yml file, or remove this
# line and fill in the details of your own MySQL/MariaDB server
DATABASE_URL="mysql://root:password@database:3306/app?serverVersion=10.6.19.2-MariaDB&charset=utf8mb4"
# Fill in your MySQL/MariaDB connection details
#DATABASE_URL="mysql://<mysql user>:<mysql pass>@<mysql host>:3306/<mysql db name>?serverVersion=10.6.19.2-MariaDB&charset=utf8mb4"
# Enter your Real Debrid API key
# This key is never saved anywhere
# else and is passed to Torrentio
# to retrieve download options
REAL_DEBRID_KEY=""
# Enter your TMDB API key
# This is used to provide rich search results
# when searching for media and rendering the
# Popular Movies and TV Shows section.
TMDB_API=""
# Use your own Redis instance or use the
# below value to use the container included
# in the example compose.yml file.
REDIS_HOST="redis://redis"
### Auth ###
# Change to "oidc" to and provide the required
# environment variables below to use OIDC auth.
AUTH_METHOD=form_login
# OIDC
OIDC_WELL_KNOWN_URL=
OIDC_CLIENT_ID=
OIDC_CLIENT_SECRET=
# Allows you to skip the login page and directly
# rely on your IdP for auth.
OIDC_BYPASS_FORM_LOGIN=
# LDAP Config: To use LDAP, enter the below fields
# and run 'php bin/console config:set auth.method ldap'
# (LDAP is still in progress and not ready for use)
#LDAP_HOST=
#LDAP_PORT=
#LDAP_ENCRYPTION=
#LDAP_BASE_DN=
#LDAP_BIND_USER=
#LDAP_BIND_PASS=
#LDAP_DN_STRING=
#LDAP_UID_KEY="uid"
# LDAP group that identifies an Admin
# Users with this LDAP group will automatically
# get the admin role in this system.
#LDAP_ADMIN_ROLE_DN=""
#LDAP_EMAIL_ATTRIBUTE=mail
#LDAP_USERNAME_ATTRIBUTE=uid
#LDAP_NAME_ATTRIBUTE=displayname

94
docs/examples/compose.yml Normal file
View File

@@ -0,0 +1,94 @@
services:
app:
image: code.caldwell.digital/home/torsearch-app:latest
ports:
- '8006:80'
env_file:
- .env
volumes:
- ./downloads/movies:/var/download/movies
- ./downloads/tvshows:/var/download/tvshows
environment:
TZ: America/Chicago
MERCURE_PUBLISHER_JWT_KEY: '!ChangeThisMercureHubJWTSecretKey!'
MERCURE_SUBSCRIBER_JWT_KEY: '!ChangeThisMercureHubJWTSecretKey!'
depends_on:
database:
condition: service_healthy
# Downloads happen in this container. Replicate this
# container to run multiple downloads simultaneously.
# Map your "movies" folder to /var/download/movies
# Map your "TV shows" folder to /var/download/tvshows
# If your folders are on another machine, use an NFS volume.
# This container runs a Symfony worker process.
# See: https://symfony.com/doc/current/messenger.html
worker:
image: code.caldwell.digital/home/torsearch-worker:latest
volumes:
- ./downloads/movies:/var/download/movies
- ./downloads/tvshows:/var/download/tvshows
environment:
TZ: America/Chicago
command: -vv --time-limit=3600 --limit=10
env_file:
- .env
restart: always
depends_on:
app:
condition: service_healthy
# This container handles the monitoring for new media. When new
# monitors are added, jobs are periodically dispatched to this
# container, and the desired media is searched for and downloaded.
# This container runs a Symfony worker process.
# See: https://symfony.com/doc/current/messenger.html
scheduler:
image: code.caldwell.digital/home/torsearch-scheduler:latest
volumes:
- ./downloads/movies:/var/download/movies
- ./downloads/tvshows:/var/download/tvshows
env_file:
- .env
command: -vv
environment:
TZ: America/Chicago
restart: always
depends_on:
app:
condition: service_healthy
database:
image: mariadb:10.11.2
volumes:
- mysql:/var/lib/mysql
environment:
MYSQL_DATABASE: app
MYSQL_USERNAME: app
MYSQL_PASSWORD: password
MYSQL_ROOT_PASSWORD: password
healthcheck:
test: [ "CMD", "mysqladmin" ,"ping", "-h", "localhost" ]
interval: 5s
timeout: 5s
retries: 10
redis:
image: redis:latest
volumes:
- redis_data:/data
command: redis-server --maxmemory 512MB
restart: unless-stopped
# **Optional**
# Provides a simple method of viewing the database
adminer:
image: adminer
ports:
- "8081:8080"
volumes:
mysql:
mercure_config:
mercure_data:
redis_data:

View File

@@ -0,0 +1,115 @@
services:
# The "entrypoint" into the application. This reverse proxy
# proxies traffic back to their respective services. If not
# running behind a reverse proxy inject your SSL certificates
# into this container.
# This container runs the actual web app in a php:8.4-fpm
# base container.
app:
image: code.caldwell.digital/home/torsearch-app:latest
ports:
- '8006:80'
configs:
- env_file
deploy:
replicas: 2
depends_on:
- database
# Downloads happen in this container. Replicate this
# container to run multiple downloads simultaneously.
# Map your "movies" folder to /var/download/movies
# Map your "TV shows" folder to /var/download/tvshows
# If your folders are on another machine, use an NFS volume.
# This container runs a Symfony worker process.
# See: https://symfony.com/doc/current/messenger.html
worker:
image: code.caldwell.digital/home/torsearch-worker:latest
configs:
- source: env_file
target: /app/bin/.env.local
volumes:
- /mnt/media/downloads/movies:/var/download/movies
- /mnt/media/downloads/tvshows:/var/download/tvshows
restart: always
command: -vv
deploy:
replicas: 4
depends_on:
- app
# This container handles the monitoring for new media. When new
# monitors are added, jobs are periodically dispatched to this
# container, and the desired media is searched for and downloaded.
# This container runs a Symfony worker process.
# See: https://symfony.com/doc/current/messenger.html
scheduler:
image: code.caldwell.digital/home/torsearch-scheduler:latest
configs:
- env_file
volumes:
- ./downloads:/var/download
restart: always
depends_on:
- app
# This container facilitates viewing the progress of downloads
# in realtime. It also handles sending alerts and notifications.
# The MERCURE_PUBLISHER_JWT key & MERCURE_SUBSCRIBER_JWT_KEY should
# match the MERCURE_JWT_SECRET environment variable.
mercure:
image: dunglas/mercure
restart: unless-stopped
ports:
- "3001:80"
environment:
SERVER_NAME: ':80'
MERCURE_PUBLISHER_JWT_KEY: '!ChangeThisMercureHubJWTSecretKey!'
MERCURE_SUBSCRIBER_JWT_KEY: '!ChangeThisMercureHubJWTSecretKey!'
MERCURE_EXTRA_DIRECTIVES: |
cors_origins *
anonymous
command: /usr/bin/caddy run --config /etc/caddy/dev.Caddyfile
volumes:
- mercure_data:/data
- mercure_config:/config
database:
image: mariadb:10.11.2
volumes:
- mysql:/var/lib/mysql
environment:
MYSQL_DATABASE: app
MYSQL_USERNAME: app
MYSQL_PASSWORD: password
MYSQL_ROOT_PASSWORD: password
healthcheck:
test: [ "CMD", "mysqladmin" ,"ping", "-h", "localhost" ]
interval: 5s
timeout: 5s
retries: 10
redis:
image: redis:latest
volumes:
- redis_data:/data
command: redis-server --maxmemory 512MB
restart: unless-stopped
# **Optional**
# Provides a simple method of viewing the database
adminer:
image: adminer
ports:
- "8081:8080"
volumes:
mysql:
mercure_config:
mercure_data:
redis_data:
configs:
env_file:
file: $PWD/.env

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 421 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 654 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 568 KiB

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