726 Commits

Author SHA1 Message Date
488da889d8 Current state of graphql implementation. Some things are broken. Focused on getting progress from deluge linked with requests and requested torrents. Warning! the payload size of this action is 1MB pr second. This also heavily dependes on un-tracked changes in both delugeClient and the frontend for the visualization. 2019-11-21 22:32:08 +01:00
8da7c159b1 WIP graphql to get requests, torrents and working on requests with torrents by username. 2019-11-11 16:54:54 +01:00
ea5bc36956 Merge pull request #111 from KevinMidboe/api/v2
Api/v2
2019-11-04 18:01:15 +01:00
fd475265c1 Updated test to reflect changes to all error response objects. (changed from returning message in error key to message. 2019-11-04 17:54:08 +01:00
b0804f8a08 Merge branch 'api/v2' of github.com:kevinmidboe/seasonedShows into api/v2 2019-11-04 00:57:45 +01:00
6b737b8ab4 Updated all controller responses to return message not error on errors. 2019-11-04 00:57:27 +01:00
5623344666 Merge branch 'master' into api/v2 2019-11-03 20:57:25 +01:00
f8cc19b510 Added rating and release_date when parsing movies and fixed respective tests 2019-11-03 20:56:46 +01:00
c589457a6c Removed unused mongoose package 2019-11-03 20:55:54 +01:00
b802a7b62b Moved, renamed, re-did and added a lot of stuff. Getting ready for the v2 upgrade 2019-11-03 20:33:30 +01:00
879a02b388 Finished movie credits and release dates 2019-11-03 16:01:19 +01:00
bc3d4881bd New release_dates endpoint for movie 2019-11-03 15:43:35 +01:00
ef8d4d90b2 Removed console log 2019-11-03 15:08:42 +01:00
d2d396bb7a Set cache TTL for credits to 1 day 2019-11-03 15:07:11 +01:00
500b75eaf6 We know there could be a 401 response so we handle it 2019-11-03 15:04:14 +01:00
9308d4ea9b Credits endpoint for movies 2019-11-03 15:02:45 +01:00
6c2c81a1a1 Updated movieInfo controller to also handle requesting release_dates as query parameter 2019-10-04 22:36:39 +02:00
90aa4d2485 Rewrote the movie & show list controller to be more abstract and easier to extend later 2019-10-04 21:21:52 +02:00
0ca3f81bf8 listController first defines all async functions as constant variables then module exports them all as a dict 2019-10-04 20:55:39 +02:00
b9831c6b3d Forgot to reasing variables after copy-pasting them in convertTmdbToMovie 2019-10-04 20:37:06 +02:00
4781e9ae65 Merge pull request #119 from KevinMidboe/snyk-fix-a43be890852096db7a469faa6c44f8ef
[Snyk] Fix for 3 vulnerable dependencies
2019-07-27 02:17:50 +02:00
snyk-test
eb0881f19e fix: client/package.json to reduce vulnerabilities
The following vulnerabilities are fixed with an upgrade:
- https://snyk.io/vuln/SNYK-JS-JSYAML-173999
- https://snyk.io/vuln/SNYK-JS-JSYAML-174129
- https://snyk.io/vuln/npm:mem:20180117
2019-07-27 00:16:09 +00:00
bc4d73821d Upgraded webpack-dev-server to not have a screaming vulnerability 2019-07-27 02:05:38 +02:00
ab6144eb81 Update yarn lock, updated coveralls and mocha run under coverage command now uses required babel register 2019-07-27 02:03:11 +02:00
c3d87e2200 Merge pull request #117 from KevinMidboe/snyk-fix-9429d5dfb86996c66ac12aef1a5178b1
[Snyk] Fix for 2 vulnerable dependencies
2019-07-27 01:58:33 +02:00
e391ce7ef9 Merge pull request #112 from KevinMidboe/snyk-fix-5oeu5e
[Snyk] Fix for 4 vulnerable dependencies
2019-07-27 01:58:07 +02:00
ca707078d9 Merge branch 'master' into snyk-fix-5oeu5e 2019-07-27 01:57:08 +02:00
snyk-test
53228a2662 fix: client/package.json to reduce vulnerabilities
The following vulnerabilities are fixed with an upgrade:
- https://snyk.io/vuln/SNYK-JS-JSYAML-173999
- https://snyk.io/vuln/SNYK-JS-JSYAML-174129
2019-07-26 23:56:40 +00:00
2a9fa27341 Merge pull request #113 from KevinMidboe/snyk-fix-25dne6
[Snyk] Fix for 1 vulnerable dependencies
2019-07-27 01:56:24 +02:00
3068281461 Update README.md 2019-07-27 01:55:43 +02:00
81e9fe5b15 Testing for a running application is a bit weird, disabling it for now 2019-07-27 01:42:44 +02:00
5d2e375213 Upgraded node version for travis from 8 to 11 2019-07-27 01:34:15 +02:00
7ede37039a info-success-response is now a list so need this reflected in this test also. Changed the port we test for to whats in our config/test.jsono 2019-07-27 01:30:08 +02:00
8e23ae5a27 Added babel for ES6 functionality. In this case the new import statements 2019-07-27 01:28:41 +02:00
04ba094a14 Throw more errors and cleanup some unmerged code 2019-07-26 21:55:59 +02:00
23f9911237 Throw errors when failing to create user 2019-07-26 21:54:13 +02:00
3b27af1f83 Error handling for themoviedb api response codes that are not 200. Started with 401 and 404. See issue #116 for info. 2019-07-26 21:52:20 +02:00
afb7af46b8 The test uses the cached to not need to query themoviedb, but the function that would in prod now saves a Class containing movie result and extras such as credits. 2019-07-26 21:09:58 +02:00
6ba8ca2add Updated search query response 2019-07-26 21:07:26 +02:00
135375cb94 instead of "describe" "xdescribe" was defined which made the test pending in results. This has now been resolved. 2019-07-26 21:06:45 +02:00
e5d5bdefd6 Updated cache key and cleaned up formatting 2019-07-26 21:05:45 +02:00
6f9ca9e067 Added package and commands for generating documentation and upgraded mocha 2019-07-26 20:56:33 +02:00
c42195d242 Removed swap file 2019-07-25 00:53:40 +02:00
a5aaf1bfca Merge branch 'api/v2' of github.com:KevinMidboe/seasonedShows into api/v2 2019-07-25 00:53:16 +02:00
af7b1f2424 Script for updating all requested and downloading request status to downloaded if exist in plex 2019-07-25 00:48:16 +02:00
6aba9774c6 When requesting all request elements we now also return the page as int not string 2019-07-25 00:47:17 +02:00
e19cfb5870 Updated formatting 2019-07-25 00:24:04 +02:00
144b27f128 Renamed token variable from user to username 2019-07-25 00:23:32 +02:00
12afbf6364 Tokens can also have a admin property. When admin is defined its included in the jwt token. 2019-07-25 00:13:28 +02:00
8a5ab204e1 Change node bcrypt package from bcrypt-nodejs to bcrypt. Change response message on invalid username/pass and changed to bcrypt syntax for compare and hash. 2019-07-24 23:02:06 +02:00
3f04d9bc56 Update script for updating all plex statuses of requestes. 2019-07-24 23:02:06 +02:00
de50805d1e Handle both status code 301 and 302 from jackett 2019-07-15 18:16:46 +02:00
3a9131a022 Removed, commented and added comments 2019-07-02 23:53:26 +02:00
77433e8505 Query of plex search is now encoded 2019-07-02 23:53:08 +02:00
3845000b3f Allow filtering for requested items by status 2019-06-28 23:32:11 +02:00
071fd54825 Pages for requests are only calulated for items not yet downloaded and use ceil isteadof floor to get last items on a page 2019-06-28 22:51:09 +02:00
537f237e83 Updated lock file 2019-06-28 22:45:31 +02:00
d3bc854e03 Pirate repository has relative and use virtuale python when running python commands 2019-06-28 22:44:39 +02:00
15826a00ba plexRepo now using class instance plexIp address 2019-06-28 22:42:11 +02:00
4019d63f3b Created example config and added development config to gitignore 2019-06-28 22:39:24 +02:00
91dcfaccb9 Rewrote posting request controller to handle body parameters and use new requestsRepository function istead of plexRepository functions 2019-06-28 22:31:52 +02:00
270a259cee Request list also gets and returns total pages 2019-06-28 21:51:43 +02:00
162d20ae52 Submitting requests now use requests repository 2019-06-28 21:51:11 +02:00
9f1badc1b1 Get a request item by id and type 2019-06-28 19:21:54 +02:00
ac027a97d6 Added pagination and removed sort & filtering for requested items 2019-06-28 18:50:30 +02:00
127db88ded Renamed function name and comment to make for sense. Also deconstruct page from query 2019-06-28 18:45:53 +02:00
4b07434615 Imported configuration with incorrect name 2019-06-05 00:05:54 +02:00
5d6f2baa34 plex hook should have post not get 2019-06-05 00:00:31 +02:00
1a1a7328a3 Map results with total_results before returing. TODO this should be mapped with all wanted list return vars 2019-06-04 23:54:39 +02:00
b9dec2344e Added timeout to plex requests and include error in error message when unable to search 2019-06-04 23:53:54 +02:00
476a34fb69 Changed the order of execution between getting tmdb movie and searching plex for it. Now we await tmdb movie and then check if exists in plex. This is better when we miss plex request 2019-06-04 23:47:10 +02:00
e3ed08e8dd Now a plex ip address is dynamically passed into the plexrepository, fetched from the config 2019-06-04 23:45:22 +02:00
70f6497404 All converter function from tmdb to movie, show and person takes optional cast object and maps to response 2019-06-04 23:35:21 +02:00
99bab3fb73 Movie and show can also return credits for a item. Enabled by query parameter credits=true 2019-06-04 23:32:38 +02:00
e6796aff8b Hotfix 🧯 for returning new poster object variable 2019-06-04 21:41:10 +02:00
snyk-bot
4eaa60b044 fix: seasoned_api/package.json to reduce vulnerabilities
The following vulnerabilities are fixed with an upgrade:
- https://snyk.io/vuln/SNYK-JS-MPATH-72672
2018-12-13 03:19:03 +00:00
snyk-bot
7db8f752c5 fix: seasoned_api/package.json to reduce vulnerabilities
The following vulnerabilities are fixed with an upgrade:
- https://snyk.io/vuln/npm:base64url:20180511
- https://snyk.io/vuln/npm:cryptiles:20180710
- https://snyk.io/vuln/npm:extend:20180424
- https://snyk.io/vuln/npm:stringstream:20180511
2018-12-08 03:59:04 +00:00
784aa2616a Fetchall gets docstring 2018-11-12 01:20:18 +01:00
7cb55ce054 Fetchall uses promises smarter. Now the utils functions also return promises to be able to nicely chain the steps a request needs to go through. Promise all lets us wait for all items return in the map function. Without the map function would return immidiately and resolve before the map operation completed. 2018-11-10 20:26:28 +01:00
87eb6de802 🔨 test now requests with id in body not in query params. 2018-11-10 01:57:19 +01:00
840816c930 request returns all requested items. Optional sort, query and filter params. 2018-11-10 01:50:24 +01:00
91d238de7c Request id is now passed as body param. Database default timestamp value changed to epoch time. 2018-11-09 22:13:00 +01:00
0ac17d3d0a Removed unused const declaration. 2018-11-01 00:24:47 +01:00
87c76e3f1d Tests now suppoer the new list endpoints. Also updated response for interstellar query (movieInfo). 2018-11-01 00:18:54 +01:00
e64c4d5d01 Lists are now reachable by movie or show / listname. Endpoints added & removed outdated comments. 2018-11-01 00:17:51 +01:00
22e57c03de Controller for movie and shows. Each have multiple small export functions; one for each list search type 2018-11-01 00:16:56 +01:00
d80386da40 Implementing lists lookups for movie and shows. Add new cachetags for the lists & created a helper function for returning response with convertFunction as parameter. 2018-11-01 00:15:49 +01:00
e7c66af3f6 Merge branch 'master' into api/v2 2018-10-30 21:02:47 +01:00
8ece7b84c4 test configuration also gets plex ip parameter. 2018-10-30 20:38:05 +01:00
4250b1bd17 request endpoint finds type by body not query. Better error handling on what goes wrong if incorrect type or missing body parameter. 2018-10-30 20:34:26 +01:00
7e46d32e30 More unit tests for checking correct definition of movie. Changed some test to match update functions in tmdb. 2018-10-30 20:32:55 +01:00
5a48158f07 Request now happens at /request with id parameter and query for type selection. Only allows movie or show type and is static set in the controller. AddRequest adds tmdb item to database with time of request. 2018-10-30 19:20:52 +01:00
161a466ab7 Rewrote how local plex library is indexed and what it returns. After searching plex the response is separated into three classes by types (movie, show & episode). Plex also has a function for inputing a (tmdb)movie object and searching for matches of name & type in plex. If a match the object matchedInPlex variable is set to true. 2018-10-29 21:01:16 +01:00
8f5bd44e4d Added endpoint for new plex search. 2018-10-29 20:57:22 +01:00
5d8869e042 Rewrote every function for searching and looking up items from tmdb library. Now there are separate functions for the four categories of search and three for info (multi, movie, show & person). Each function now has its own endpoint and matching controller. Converting tmdb results into a class has been alterted from using three classes; movie, show & person, and each have each their own convertTmdbTo function. Now the structure of the three types are more structured and no longer a single "seasoned" class object. 2018-10-29 20:55:18 +01:00
90b8ee005e Changed moviedb package to my own fork of it. The old package had vulnerabilities and needed updating. 2018-10-29 20:49:21 +01:00
1b0525063f New parameter in config and added axios package for new plex connect command. 2018-10-29 20:47:57 +01:00
41d6bba743 v2 endpoints added for clearer intent in endpoints. Two new controller categories; info and search. 2018-10-28 12:21:47 +01:00
8977a4b195 Merge pull request #109 from KevinMidboe/package/upgrade
Changed moviedb node package to my own fork (km-tmdb) with updated to…
2018-10-26 01:01:43 +02:00
7e0da028de Imported new version of moviedb package 2018-10-26 00:59:22 +02:00
2250cf2c4b Changed moviedb node package to my own fork (km-tmdb) with updated to vulnerability in the superagent package 2018-10-26 00:20:37 +02:00
b2bd7b6a1f Update README.md 2018-08-27 11:58:16 +02:00
a2ad7f5628 Removed old content 2018-08-27 11:56:28 +02:00
f85d31991f Removed versioneye badge 2018-08-27 11:54:48 +02:00
08dc2153ae Updated build config for codeclimate test reporting. 2018-08-26 10:56:50 +02:00
bc64e69b3e Fixed syntax error. 2018-08-13 00:01:51 +02:00
a29bca7361 Controller now expects three parameters for posting to addMagnet; magnet, name and tmdb_id. 2018-08-12 23:59:26 +02:00
d84aa5f173 Merge branch 'master' of github.com:KevinMidboe/seasonedShows 2018-08-12 23:55:40 +02:00
48ebd398bc Changed name values of tables. 2018-08-12 23:50:07 +02:00
1b95103acd Removed test that caused breaking changes. 2018-08-12 23:31:18 +02:00
6a1d6687eb Updated gitmodules 2018-07-28 19:00:32 +02:00
e849864bc2 Added delugeClient to gitmodules. 2018-07-28 18:55:39 +02:00
ecc2a67d48 Updated readme for cloning requrse submodules. 2018-07-28 17:55:28 +02:00
bfe0d55f71 Excetion node-pre-gyp failed for version check for sqlite3. The package was set to static version and needed a patch because of errors caused by newer versions of node. 2018-07-28 17:51:59 +02:00
634d4513eb Merge pull request #106 from KevinMidboe/remove_verified
Remove verified
2018-07-28 16:17:43 +02:00
0a1276a474 Merge pull request #105 from KevinMidboe/plex_api_update
Plex api update
2018-07-28 16:17:33 +02:00
3a34d8995e Merge pull request #104 from KevinMidboe/misc
Misc
2018-07-28 16:15:18 +02:00
918e629a06 Merge pull request #103 from KevinMidboe/stray_fixes
Stray fixes
2018-07-28 16:15:03 +02:00
7dd016a56e Changed the python run variable for stray eps. 2018-07-28 16:05:22 +02:00
c10bbcf518 Added update function to package.json fole. 2018-07-28 15:58:02 +02:00
3402a52633 Change to log the parent name of the element instead of the name of file. 2018-07-28 10:50:57 +02:00
86e9188a5c The api from plex has changed. This reflects the changes from Video to metadata in the api url. 2018-07-28 10:46:25 +02:00
8918b7906e Merge branch 'remove_verified' of github.com:KevinMidboe/seasonedShows into HEAD 2018-05-13 19:21:52 +02:00
7e028a461d Merge branch 'master' of github.com:KevinMidboe/seasonedShows into HEAD 2018-05-13 19:21:14 +02:00
fe5f0c815e Deluge is now imported and a Deluge class is created and remove function called for name of torrent that is being verified. 2018-05-13 19:18:45 +02:00
d02e79e59e Merge pull request #100 from KevinMidboe/linting
Linting
2018-05-09 11:00:02 +02:00
5b49216c9d Fixed linting issues for json objects and tailing semicolon. 2018-05-09 10:52:08 +02:00
657ab10034 Removed unused comments. 2018-05-06 18:36:26 +02:00
ed07c77b13 Updated files with tripple === and some linting issues. 2018-05-06 18:23:29 +02:00
4fe85d9fae Change the ordering of user requests to be newest-first. 2018-05-06 18:15:54 +02:00
b99b5b32ec Updated to use abspath of the file not the call location. 2018-05-06 18:09:13 +02:00
e8058c5e4c Update README.md 2018-04-19 21:13:14 +02:00
7332b7d474 Update README.md 2018-04-19 20:22:26 +02:00
7980f14426 Update README.md 2018-04-19 20:21:44 +02:00
65540fafbd Merge pull request #99 from KevinMidboe/dependency_update
Dependency update
2018-04-10 11:29:56 +02:00
64ede43dec Added custom rule for object-shorthand and comma-dangle. 2018-04-08 11:58:16 +02:00
bed12cff72 Set branch to master for the submodule torrent_search 2018-04-08 11:50:15 +02:00
59e7f96643 Updated submodule torrent_search with new commits. 2018-04-08 11:43:12 +02:00
71e9a5a46e Merge pull request #98 from KevinMidboe/strayParser_fix
Updated when parsing for show name allow names with numbers.
2018-04-06 16:27:42 +02:00
fce6dc7658 Updated when parsing for show name allow names with numbers. 2018-04-06 16:22:58 +02:00
baff59181c Update README.md 2018-04-04 23:22:18 +02:00
0592cca16b Updated string_decoder. 2018-04-04 23:08:00 +02:00
e4d5f5085c Updated yarn lockfile. 2018-04-04 14:41:01 +02:00
66a2a06f9b Compile error, if-statement. 2018-04-03 23:02:54 +02:00
e984feeb8d Would fail if the parent was not a directory. 2018-04-03 23:00:14 +02:00
490d015f80 Update README.md 2018-03-30 15:31:15 +02:00
f1cc2c4ebe Merge pull request #97 from KevinMidboe/coverage
Added coverage stats with nyc and coveralls
2018-03-22 18:30:12 +01:00
2f4421d9e0 Merge branch 'coverage' of github.com:kevinmidboe/seasonedShows into coverage 2018-03-22 18:25:01 +01:00
92cc094787 Removed old coverage script and updated travis config. 2018-03-22 18:24:42 +01:00
f30b46c384 Update README.md 2018-03-22 17:20:14 +01:00
d9f679603a Command in travis config was doc, not cov. 2018-03-22 17:17:23 +01:00
64bd9d1e14 Added support for travis upload test coverage to coveralls. 2018-03-22 15:09:59 +01:00
721826d454 Re-added request and removed superagent. 2018-03-22 13:24:57 +01:00
242fe3515c Removed and moved some dep to dev. 2018-03-22 13:23:30 +01:00
ccf40d2161 Merge pull request #96 from KevinMidboe/snyk-fix-6de5e99f
[Snyk] Fix for 42 vulnerable dependency paths
2018-03-22 12:59:36 +01:00
832b8ba539 Added license and description to package.json. Also updated packages node packages. 2018-03-22 12:56:04 +01:00
snyk-bot
0477e49eca fix: seasoned_api/package.json to reduce vulnerabilities
The following vulnerabilities are fixed with an upgrade:
- https://snyk.io/vuln/npm:qs:20170213
- https://snyk.io/vuln/npm:qs:20140806
- https://snyk.io/vuln/npm:qs:20140806-1
- https://snyk.io/vuln/npm:mime:20170907
- https://snyk.io/vuln/npm:mime:20170907
- https://snyk.io/vuln/npm:fresh:20170908
- https://snyk.io/vuln/npm:debug:20170905
- https://snyk.io/vuln/npm:ms:20170412
- https://snyk.io/vuln/npm:qs:20170213
- https://snyk.io/vuln/npm:negotiator:20160616
- https://snyk.io/vuln/npm:ms:20151024
- https://snyk.io/vuln/npm:debug:20170905
- https://snyk.io/vuln/npm:ms:20170412
- https://snyk.io/vuln/npm:ms:20151024
- https://snyk.io/vuln/npm:hoek:20180212

Latest report for kevinmidboe/seasonedshows:seasoned_api/package.json:
https://snyk.io/test/github/kevinmidboe/seasonedshows?targetFile=seasoned_api/package.json
2018-03-22 11:34:42 +00:00
451b67630a Merge pull request #95 from KevinMidboe/docs_testing
Improved testing, more linting and added paging to request/all endpoint
2018-03-21 23:53:29 +01:00
096bbdf085 Merge branch 'master' into docs_testing 2018-03-21 23:51:31 +01:00
e914e4ab45 Added ORDER BY date that was missing in sql stmt 2018-03-21 23:44:59 +01:00
c1461e1f41 Merge pull request #93 from KevinMidboe/travis
Updated travis to pull submodules
2018-03-21 14:43:36 +01:00
91bf2c1e2a Updated travis to pull submodules 2018-03-21 14:30:26 +01:00
da3df383ed Update gitmodule torrent_search url to be https 2018-03-21 14:29:51 +01:00
9816b978d3 Updated demo on readme 2018-03-21 12:13:57 +01:00
8e22b0f6ea Updated fixtures. 2018-03-20 21:18:11 +01:00
18359f442c Mapped results in tmdb now returns the complete json object so not needed to be created before sent. When getting all requested movies and shows it is now possible to only get one page at a time. 2018-03-20 21:17:41 +01:00
42b8b5ea0e Removed a ) 2018-03-20 20:15:28 +01:00
996295b1fe Removed the id parameter, not used. 2018-03-20 20:14:44 +01:00
1b08c8d3d1 Now when running tests we use a separate config file test.json 2018-03-20 20:14:15 +01:00
9e145f7068 When testing we dont have api access so we create cache entries before each request. The cache key changed so this is now updated in the tests and tmdb. 2018-03-20 20:12:49 +01:00
7051edb212 Added unit tests for testing config file for correct values and if not check the env variables. 2018-03-20 19:25:35 +01:00
0581813ee3 Merge pull request #92 from KevinMidboe/api_filterBug
Fixes api not filtering requests/all query when filtering
2018-03-20 13:17:17 +01:00
edf1de223e When filtering the request on status the sql query for the db did not sort DESC. 2018-03-20 13:11:26 +01:00
7595b6df25 Update README.md 2018-03-19 17:03:03 +01:00
04ec0e0785 Update README.md
Added snyk badge
2018-03-19 17:02:02 +01:00
b08dc899c3 Update ISSUE_TEMPLATE.md 2018-03-19 16:32:03 +01:00
aafe524681 Merge pull request #90 from KevinMidboe/testing
Testing
2018-03-19 16:28:44 +01:00
40b7ceda5f The name of the teardown.sql file was renamed, reverted to correct name. 2018-03-19 16:23:58 +01:00
8850f06e3e Updated travis config. 2018-03-19 16:15:31 +01:00
972dcf4aab Merge pull request #84 from KevinMidboe/api_request
Api request
2018-03-19 15:46:18 +01:00
8e0eff7120 Merge branch 'master' into api_request 2018-03-19 15:46:07 +01:00
e092eae60a The retrieveHash and changePassword functions now return promises. 2018-03-19 15:43:48 +01:00
424cf0985d Added test for registering when user is already registered. 2018-03-19 15:43:00 +01:00
ed5d302820 Stricter when running test, now we wait until resetdatabase and createuser are done in in the before function before running the tests. 2018-03-19 15:27:11 +01:00
559ef1bc11 Before we create a db instance we check for testing flag is set and then use the :MEMORY: database for a anonymous db instance. 2018-03-19 15:25:53 +01:00
9645fba4ef Removed connect, connection is now a new sqlite instance, async run function, check for errors when executing and setup and teardown now return promises. 2018-03-19 15:24:42 +01:00
e27d5c7084 We use the db instance globally with establisheddatabase. 2018-03-19 15:23:54 +01:00
4ab3946181 Added testing variable, this uses :MEMORY: database when running tests. 2018-03-19 15:22:33 +01:00
3a508b9843 Because we sort requests by date we want to use a finer measurement of time to sort more correctly. 2018-03-09 13:37:23 +01:00
d8078570eb Changed the sort order when selecting all from requests. 2018-03-09 13:36:01 +01:00
47b499c072 Jackett sometimes returns a link to self that redirects to a magnet. The response is now parsed for if it is a link and gets the url it redirects to. 2018-03-08 16:35:17 +01:00
a69e2d9bbb Merge pull request #88 from KevinMidboe/submodules
Submodules
2018-03-08 14:31:21 +01:00
72a7a7e00d Changed path to new submodule location. 2018-03-08 14:25:28 +01:00
50d3135a81 Moved submodule to parent directory. 2018-03-08 14:15:25 +01:00
9e557759ce deleted old submodule app. 2018-03-08 14:07:57 +01:00
1806f89e2b Imported submodule from scratch again. 2018-03-08 13:59:43 +01:00
9140fa37eb Deleted gitmodules and include folder. 2018-03-08 13:57:06 +01:00
6c062f5fc1 Changed gitmodules config file and moved the submodule from app to a new directory inculde. Also changed default search site to jackett. 2018-03-08 13:48:52 +01:00
3abd7ddc61 Fixed merge conflict to newer SQL statements 2018-03-08 10:38:56 +01:00
6a7b427ef3 Changed cache TTL to 3 hours. 2018-03-08 10:36:13 +01:00
8fbd0eb9d8 Also unpacks the value of username before passing it to searchHistory. 2018-03-08 10:33:04 +01:00
127db8b2fe Errors when we have a user that was not signed in. Fixed to unpack the username value first, then pass it to the function. 2018-03-08 10:32:19 +01:00
13401a318a History was not wokring because upgrade to sqlite3. Now we do a stateless call to the database and reolve or reject the response. 2018-03-08 10:30:39 +01:00
72847efb93 Update ISSUE_TEMPLATE.md 2018-03-08 02:27:17 +01:00
7801ea5d05 Create ISSUE_TEMPLATE.md 2018-03-08 02:26:02 +01:00
37474de848 Changed to resolve and reject the result from tmdb. And first get the username before sending user to another functoin. 2018-03-07 18:14:07 +01:00
1a4aefda14 Get the user variable from req not req.headers 2018-03-07 18:12:21 +01:00
40b539db68 Reversed the sorting direction. 2018-03-07 15:37:39 +01:00
453727db1b Fixed issue where the user was not being saved alongside the request. Also fixed what the list of requests are sorted by. 2018-03-07 15:35:55 +01:00
ab2d3c6756 Renamed function name and now returns 500 error when a problem is found. 2018-03-07 14:57:18 +01:00
69e694493c updating submodule to latest 2018-03-07 09:58:54 +01:00
27c95375e2 Reverted back to only be authenticated beacuse something wrong with getting the correct user. Will replace with admin check as middleware when fixed. 2018-03-07 01:37:42 +01:00
ba96e27c43 Alongside the token the value of admin state is also sent. 2018-03-07 01:35:40 +01:00
858f3f5d57 Can now get user admin status. 2018-03-07 01:30:28 +01:00
59b206bf2d Forgot a leading / 2018-03-06 22:13:53 +01:00
fac9e0e425 Added a endpoint to fetch a random emoji. 2018-03-06 22:02:32 +01:00
e65d4125b9 Trying to sort on the wrong column name. 2018-03-06 21:27:31 +01:00
354b833375 Removed console log of table data. 2018-03-06 21:24:18 +01:00
cffba202b0 Now the server does not crash when unable to search plex. 2018-03-06 19:33:22 +01:00
d3b4a34298 Re-added filter options for later use. 2018-03-06 19:32:51 +01:00
164191f89d Fixed indentation and updated sqlite to sqlite3! 🚀 2018-03-06 19:32:21 +01:00
02c36d041a Removed console.log and fixed docstring comment. 2018-03-06 19:30:11 +01:00
404c200474 Removed unused mail conf and added a function for getting all requested elements by loggedInUser. 2018-03-06 19:29:21 +01:00
aa0094762b Now save only the username of the user in search history. 2018-03-06 19:27:18 +01:00
7c4aeb48ab When registering a token is also returned so we dont need to sign in after registration. 2018-03-06 19:26:28 +01:00
ca146d77bc Fixed error message 2018-03-06 19:25:33 +01:00
cede376349 Was not using the registered loggedInUser variable, but one passed by client 2018-03-06 19:24:58 +01:00
e11dfc7712 Renamed class name. 2018-03-06 19:23:11 +01:00
1a693ef92f Our new controller for getting a users requested items. 2018-03-06 19:21:55 +01:00
2df734bb32 ADded endpoints for checking search history and the items a user has requested, both require authentication. Changed that searching for torrents now require being admin not just signed in. 2018-03-06 19:20:32 +01:00
6619184a45 When creating a account the email field is now optional. 2018-03-06 19:18:51 +01:00
34a97c069b Updated from sqlite to sqlite3 now all functions are async so we wait for response. 2018-03-06 19:16:10 +01:00
e6a8515432 Now we can check if user is admin. This has to be set manually and now only is used for fetching torrents. 2018-03-06 19:12:21 +01:00
08b373cba0 Response results are now saved in the object results instead of torrents 2018-02-26 19:58:54 +01:00
ec0923f1c0 Can now filter the response by status. 2018-02-26 17:17:37 +01:00
4bc94ae3b7 When a element is not found in plex it now still gets a matchedInPlex value, now False when not found. 2018-02-26 16:59:56 +01:00
bf4cf8bef1 Merge pull request #83 from KevinMidboe/api
Api
2018-02-07 15:56:40 +01:00
1d8a72429d Merge branch 'api' of github.com:KevinMidboe/seasonedShows into api 2018-02-07 15:53:52 +01:00
528cbed30f Wasn't getting all the items because some elements do not have a media_type 2018-02-07 15:53:27 +01:00
7ee3ace83b Removed unused semicolon 2018-02-07 15:44:24 +01:00
ba69893e21 Removed static function. 2018-02-07 15:44:04 +01:00
ad4fa9d95a Fixed issue with types not being upheld when converting from tmdb objects. 2018-02-07 15:40:39 +01:00
28a731efbf Merge pull request #82 from KevinMidboe/testing
Testing
2018-02-07 14:41:05 +01:00
878c71ef4a Merge pull request #81 from KevinMidboe/frontend
Frontend
2018-02-07 14:38:16 +01:00
bd1e3c4455 Added test for checking popular shows list in tmdb. 2018-02-07 14:35:36 +01:00
1be8bc0319 Merge branch 'master' of github.com:kevinmidboe/seasonedShows 2018-02-07 14:25:31 +01:00
f0476b2890 Merge pull request #80 from KevinMidboe/linting
Linting
2018-02-07 14:24:43 +01:00
72294e19c5 Merge pull request #79 from KevinMidboe/api
Api
2018-02-07 14:22:50 +01:00
30b494e356 The id used for searching was somehow not a movies id and returned false. 2018-02-07 14:20:34 +01:00
b037cb23a2 Updated test to reflect new tmdb response. 2018-02-07 14:16:45 +01:00
98b10aa693 Changed the api endpoint to one that requires to be logged in. 2018-02-07 14:15:10 +01:00
3ca3c06824 Removed static tag in tmdb functions. 2018-02-07 14:13:50 +01:00
f6c504223a Now yarn lint lints all files in src not only webserver. 2018-02-07 13:53:15 +01:00
93e1ef6d99 Linted app and server.js files. 2018-02-07 13:52:39 +01:00
58449fc753 Linted all middleware. 2018-02-07 13:52:08 +01:00
34982c14fe Linted all controllers 2018-02-07 13:51:57 +01:00
81aeed86ef Linted all pirate, git, tmdb and searchHistory scripts. 2018-02-07 13:51:42 +01:00
a8ec7acff5 Linted all seasoned scripts. 2018-02-07 13:51:02 +01:00
74d143775b Linted all user files. 2018-02-07 13:50:16 +01:00
dda1db6c5f Linted all plex scripts. 2018-02-07 13:50:05 +01:00
97217a2826 Linted all media_classes 2018-02-07 13:49:37 +01:00
272300249e Linted all database files. 2018-02-07 13:49:22 +01:00
fe1ad2b1ad Linted all config files. 2018-02-07 13:49:11 +01:00
444295d5d1 Updated our eslint config file. Still extends airbnb, but have changed indent rule and dropped some other rules. 2018-02-07 13:47:11 +01:00
a40d4f7cd5 Removed movie and show in replacement with a common media.js and tmdb and plex that extend this media class. 2018-02-07 13:44:26 +01:00
6fd65fdb23 Refactoring where now we can search lists and lookup movies or shows by using the new tmdbMethod function which takes the api endpoint variable and type to return the appropriate tmdbMethod name for the matching api call to tmdb. Removed a lot of the globally defined variables. 2018-02-07 13:42:46 +01:00
7e874bbc9d Now our convert scripts use our new plex and tmdb classes. Also compacted and abstracted a lot of the code here. 2018-02-07 13:39:45 +01:00
6564948af8 Split up our plex and tmdb classes into more logical files with a common media class that it extends off of. 2018-02-07 13:38:25 +01:00
fafb5c30e0 Merge pull request #76 from KevinMidboe/api
Api
2018-02-05 22:57:22 +01:00
af64a4e28a Removed and clean up function and formatting. 2018-02-05 22:48:41 +01:00
3c263a1ae7 Now we have brand spanking new functions for lookup and searching. The idea here is to match tmdb items to plex counterparts. 2018-02-05 22:48:00 +01:00
d8b2cef18f Removed filtering of items when searching tmdb. Also added total_results object to all return statements. 2018-02-05 22:46:10 +01:00
6f626d410d Rewrote most of this class. Now we have helper functions for getting info about items in plex. Used function is inPlex where we can input a tmdb item and it append if the item exsists in plex. 2018-02-05 22:44:57 +01:00
e6b81ef0dd Merge pull request #75 from KevinMidboe/api_fixes
Api fixes
2018-02-05 15:02:10 +01:00
cfb878fa64 Fixed issue where a item would not be returned when nothing was found matching query in plex. Also moved checkId up in the request. 2018-02-05 14:55:49 +01:00
44878a295c Merge branch 'api_fixes' of github.com:kevinmidboe/seasonedShows into api_fixes 2018-02-05 14:25:25 +01:00
9b0fa88a72 Changed docstring and removed the unused port variable. 2018-02-05 14:25:13 +01:00
80582f0022 Changed so that tmdb now gets its own type, tmdbtype 2018-02-05 14:23:40 +01:00
77d7167fcb Changed so that one must not be auth to get requested elements. This might change. 2018-02-05 14:22:44 +01:00
39cff99c10 Merge branch 'master' of github.com:kevinmidboe/seasonedShows 2018-02-05 14:04:11 +01:00
3edf7d43cb Changes reflecting update in api for naming scheme. 2018-02-05 13:57:57 +01:00
4c5384ed2e We can now sort filesizes that have a human size name and not in bytes. These values will be converted and saved as byte size. 2018-02-05 13:57:27 +01:00
2c36b7eb30 Api changed that a item has title, not name. These changes are reflected here. Also removed a console.log 2018-02-05 13:56:33 +01:00
3d2fb6e28c Api changed that a item has title, not name. These changes are reflected here. 2018-02-05 13:54:02 +01:00
952fe7220e Reflected changes in api, now we receive a results json object, not requestedItems. 2018-02-05 13:53:17 +01:00
eedfe4cf3f Changed schema to reflect changes movie title from name to title. 2018-02-05 13:48:16 +01:00
cfedd4e9c8 Added ability to check if a element is previously been requested. Also changed the schema of db to store movies poster as poster_path and background as background_path. 2018-02-05 13:47:10 +01:00
cefbb5e41c Added length of elements in return statement as total_results in api response. 2018-02-05 13:44:13 +01:00
89ec9d496b Changed the ip addres of plex server 2018-02-05 13:42:35 +01:00
bd0c7b8ab5 Now also has size of the existing file. 2018-02-05 13:41:41 +01:00
0c3ad3a95e Fixed a issue where the tmdb type was not being selected correctly. (movie/show) 2018-02-02 11:51:13 +01:00
c0c9587066 Update README.md 2018-01-09 23:45:01 +01:00
6f6b38c23d Update README.md 2018-01-09 23:43:44 +01:00
3639105240 Update README.md 2018-01-09 23:43:32 +01:00
0b42cf7f12 Because the content on the landing page felt static I added a random function to pick between the four list types. 2018-01-09 23:12:27 +01:00
4f98f2fd23 Changed the distance between the search bar and the content to be a little less and added a shadow around the search bar to make it pop a bit more. 2018-01-09 23:08:59 +01:00
0addc76126 Changed the positioning of the type icon in the header and forgot the set the style of the movie info header to the correct css element, this is fixed now. 2018-01-09 23:03:53 +01:00
5d0b766d3d Fixed conflict with what site should be searched. 2018-01-09 22:53:34 +01:00
8285f14bfa Merge branch 'master' of github.com:kevinmidboe/seasonedShows 2018-01-09 22:51:26 +01:00
159de6527c Now checks the return status for if it was a success or not. 2018-01-09 22:49:59 +01:00
fe020faf21 Added underline to header and align text the left. 2018-01-09 22:49:00 +01:00
9d2b904260 Changed the app search site from jackett to piratebay and added some better handling of errors, now the server does not instantly crash on a error. 2018-01-09 22:47:59 +01:00
a4146fe055 Merge branch 'master' of github.com:KevinMidboe/seasonedShows 2018-01-09 22:47:19 +01:00
fd53150054 Changed the python path to linux requirments 2018-01-09 22:46:52 +01:00
59fe9108ae Torrent table is the component that displays the response from our torrent query search. Here we can sort the table on the columns and also filter with a query. 2018-01-09 22:44:02 +01:00
0e85046751 Update README.md
Changed travis build badge to look at master instead of testing branch.
2018-01-09 17:06:16 +01:00
aa1866bbd9 Fixed merge conflict regarding version of express 2018-01-09 17:05:02 +01:00
bc0211e34f Moved eslint to devDep and updated express to 4.11 2018-01-09 17:02:03 +01:00
34a0c7f958 Merge pull request #72 from KevinMidboe/testing
Testing
2018-01-09 16:56:45 +01:00
f6bd547701 Merge branch 'master' of github.com:kevinmidboe/seasonedShows 2018-01-09 16:55:09 +01:00
e0da166406 Our request page is redesigned with a new header and search bar. It also has a better way of redrawing when the page size is changed to a mobile device. This is not very reactonic, but this file contains the intire search and request logic of the page, but this means we still have things todo :) 2018-01-09 16:53:41 +01:00
65bce27a2b Added our torrent_search repo as a submodule in our app folder. This will need to be set up automatically with build setting when the project is to be downloaded. 2018-01-09 16:49:24 +01:00
e9f6f3a656 We added a new python app for fetching torrents. The api is now changed to use the new project. IMPORTANT that a check for if jackett is set up is done, if else this is will not run correctly. 2018-01-09 16:44:10 +01:00
b8fb718540 Added NODE_ENV environmental variable production when building. 2018-01-09 16:40:49 +01:00
21a94d88a9 Added a seperate component for the info button. 2018-01-09 16:39:16 +01:00
519eba8fda Added some css for admin component that defines the styles for parent elements of imported components. 2018-01-09 16:36:24 +01:00
10ef1bfa69 This receives a response of list of torrents as props and displays them in a table view. From here we can send to download and also filter the results by query. 2018-01-09 16:33:46 +01:00
da54ad0066 Changed import name of torrentTable to correct TorrentTable 2018-01-09 16:30:38 +01:00
c8a2ea9907 This now contains the button and request function for getting torrents matching a serach query given by the name of the elment. The result is passed to the child component torrentTable which renders the result in a table view. 2018-01-09 16:28:34 +01:00
262093d196 Completely re-wrote our sidebar. Now we can filter by search and category on top and a list of all elements are displayed below in a scrollable list. Each element has a indicator showing if the item is requested, downloading or downloaded. There are several generator functions that are to be pulled out in their seperate components. 2018-01-09 16:25:52 +01:00
e64d88bfdf The stylesheet for all of requestInfo page. 2018-01-09 16:20:41 +01:00
f7c6f6603b Renamed style variable to match the imported stylesheet. 2018-01-09 16:20:18 +01:00
279b004aad We re-wrote most off the look and feel of this page. No we get a header that is generated with all the info for a requested element. This has a indicator generated by generateStatusIndicator, showing if the element is requested, downloading or downloaded. Then we have a function for generating the type icon, that is movie/show that is displayed in the header. The summary for a movie might be long so if it is over 180 chars than it is cut and a show more button is displayed. After the info on the item we import PirateSearch that takes a name input and displays a search result for that query in a table view. 2018-01-09 16:19:43 +01:00
33cb6f5f09 In admin we added a updatehandler so that a child can update this parent element. Also added our new sidebar for filtering our requested items by query and category. 2018-01-09 16:03:11 +01:00
4d99cae74c Pulled our info button out to its own component, changed the size of the poster that is fetched from tmdb from width of 300 to 185 pixels. Also some minor changes to the info displayed, no also has type listed in the object. 2018-01-09 15:52:28 +01:00
976d5cf69f TorrentTable gets passed a json list of torrents and this displays it in a table view. 2018-01-02 15:30:23 +01:00
76069f4fea Added license badfge 2017-12-21 01:04:00 +01:00
d92e6d8c78 Psst. Removed console logs from debug hunt. 2017-12-21 01:01:43 +01:00
601440d540 Ok. I can explain this bug. It seems commenting out teardown in resetDatabase solves our issue that travis cannot find our teardown script eventough it outputs the exact path it should be at. Will come back to this as it seems it works everywhere else. Good night. 2017-12-21 01:00:47 +01:00
7824e8be27 Merge branch 'testing' of github.com:kevinmidboe/seasonedShows into testing 2017-12-21 00:55:05 +01:00
b4352ad721 Too many ; bro 2017-12-21 00:53:38 +01:00
9e160a7a91 Added build status to radme 2017-12-21 00:52:34 +01:00
2623649c5d Tried to re-add teardown and updated it to drop only tables that are in the database. Crossing fingers for a second pass. 2017-12-21 00:46:52 +01:00
908fca6dd0 Trying to remove teardown between tests. 2017-12-21 00:42:44 +01:00
e6a34a0503 Back to linux. plz fix plzzzzz 2017-12-21 00:38:46 +01:00
f0e8f84e12 Building with osx now 2017-12-20 23:20:49 +01:00
0459fb9312 More testing for this bug. 2017-12-20 23:18:13 +01:00
6a725e3921 Added tons of console.logs to try find out where the problem with travis not being able to read file lies. 2017-12-20 23:06:02 +01:00
fb22576708 Travis is still not reading our teardown file. 2017-12-20 22:51:08 +01:00
bced4e052d There was a weird thing with the file permissions so it would not be read by travis. Now it should be fixed 2017-12-20 22:42:53 +01:00
54eca33dfa Forgot to update the node version in travis config file. 2017-12-20 22:27:20 +01:00
93e43d9954 Added a template development.json file for build purposes. 2017-12-20 22:22:25 +01:00
fba3845523 Now we check if the values length is 0 not the filters varaible. 2017-12-20 22:17:45 +01:00
fe64af856e Fixed a old bug. 2017-12-20 22:16:57 +01:00
625717f7ad Now when checking config file if the value is blank it checks env variables for a variable defined as uppcase section + option with a separator of _. 2017-12-20 22:16:36 +01:00
e4573e6808 Forgot to remove path without test scripts yet. 2017-12-20 21:13:24 +01:00
e19b604a78 Our teardown sql script. 2017-12-20 21:08:35 +01:00
f6c27482e4 Added script for teardown of database. 2017-12-20 21:07:33 +01:00
00ad5cf7a8 Added mocha, istanbul and supertest packages. Added coverange and test script. 2017-12-20 21:07:13 +01:00
ba33552099 Merge pull request #71 from KevinMidboe/testing
Testing
2017-12-20 20:55:18 +01:00
99ddb61c37 Started by adding some system tests for the api. 2017-12-20 20:47:32 +01:00
790f5d6588 Added travis support to api. 2017-12-20 20:46:15 +01:00
a51a78bc71 Merge branch 'master' of github.com:kevinmidboe/seasonedShows 2017-12-03 15:01:50 +01:00
317aa69f18 Re-added a CSS class that was removed. 2017-12-02 15:42:23 +01:00
de90b48430 Update README.md 2017-12-02 15:39:02 +01:00
6011615a41 Merge pull request #68 from KevinMidboe/add-metadata-requests
Add metadata requests
2017-12-02 13:23:24 +01:00
0338e90bf7 Merge pull request #66 from KevinMidboe/styling
Styling
2017-12-02 13:19:54 +01:00
cf43500c01 Merge branch 'master' into styling 2017-12-02 13:19:00 +01:00
c65cdd84b7 Changed our gitignore file for the api folder. 2017-12-02 13:18:20 +01:00
7849a8ce3f Fixed syntax error. 2017-12-02 13:17:17 +01:00
246abd7020 Renamed the variable name for our stylesheet. 2017-12-02 13:16:57 +01:00
580cdc430f Removed a lot of unused css classes. 2017-12-02 13:15:23 +01:00
7bde2821d0 Followed with the renaming of MovieObject to now be refered to as SearchObject. Also change the loading animation for InfiniteScroll. It is the same loading animation imported as we have in torrent search. 2017-12-02 13:13:35 +01:00
45db534681 When a requested element from the sidebar is selected this is where the detailed info is displayed. Now it is possible to change the status of a item by using the dropdown. This is where piratesearch also gets the query name through props. 2017-12-02 12:17:00 +01:00
fd0a2c9d50 Renamed the exporting class name to match the file name. 2017-12-02 12:08:47 +01:00
e55067025e Renamed movieObject to searchObject, same with css. 2017-12-02 12:05:17 +01:00
187ac6317e Still a bit messy, but removed unsued sections and redid the naming schema for css items. 2017-12-02 12:01:49 +01:00
9fcc82d7cf Put both jsx and js varaibles on same line in config file. 2017-12-02 11:35:39 +01:00
c11b222ee3 Deleted unused svg image. 2017-12-02 11:35:10 +01:00
8695a553d6 Admin page is our landing page for admin panel, now it passes our api response to sidebar and info panel when we get the response back from the api. Some renaming of stylesheet variable. 2017-12-02 11:32:49 +01:00
3fe8f46dd4 Viewport changes to limit zoom in on mobile the typing in a search bar. 2017-12-02 11:31:25 +01:00
5fb1e7ba2e Sidebar is now redesigned with color indicators, filtering name by search query or filtering status by dropdown. Moved CSS to a separate stylesheet. 2017-12-02 11:20:26 +01:00
e1fed24fc0 Added a loading image to pirateSearch. Now we get feedback when waiting for response from the api. 2017-12-02 10:39:18 +01:00
8542da34cd Added a separate stylesheet for our buttons. 2017-12-02 10:37:31 +01:00
7d44f1518f Updated gitignore to ignore conf folder and deleted the contents of conf. 2017-12-01 10:57:28 +01:00
fa8f82449b Merge branch 'master' of github.com:KevinMidboe/seasonedShows 2017-12-01 10:49:08 +01:00
f7b772a170 Changed the default cache TTL from 7 days to 2 days. 2017-12-01 10:46:50 +01:00
d0597b4e1e This is our main focus on changing, this is where we add a background parameter in our database entry. 2017-12-01 10:34:09 +01:00
db11c968a3 Renamed folder the plural. 2017-12-01 10:28:29 +01:00
b8647f982d This is where we use our setup function to add tables if none are present in our database file. 2017-12-01 10:27:46 +01:00
ac6bbe6ac6 Now we can build or database by reading schema files. Also added doc strings to all the functions. This is a much better definition of a database script for our usecases.. 2017-12-01 10:19:22 +01:00
2b772e3017 Long overdue, now we define our database schema so we can finally build our database, and not just move it around. 2017-12-01 10:14:22 +01:00
57658f10c1 Used the wrong shows.db. Now corrected. 2017-11-28 22:25:06 +01:00
62b6f5c8ca Using react-interactive we now have hover animations for our buttons. 2017-11-28 21:02:25 +01:00
362e5f54d1 Added react-interactive package to make it easier to handle hover and focus on mobile. 2017-11-28 20:58:45 +01:00
c5938c3c78 Merge pull request #63 from KevinMidboe/sentry
Sentry
2017-11-28 19:26:18 +01:00
2421b04548 Removed last commented line. 2017-11-28 19:25:23 +01:00
ff78dfbd50 Cleaned up in the main gitignore file and moved seasoned_api related ignore to its child file. 2017-11-28 19:24:47 +01:00
36b4f16047 Revert "Removed files."
This reverts commit 7f4b95c8c4.
2017-11-28 19:22:40 +01:00
5da57062a7 Added a gitignore for each main folder because we have python based scripts in one folder, javascript/react and javascript/node in the other. 2017-11-28 19:19:50 +01:00
be4c79ca16 Added env folder to gitignore 2017-11-28 19:10:45 +01:00
613b4ecee3 Merge pull request #62 from KevinMidboe/sentry
Sentry
2017-11-08 14:04:37 +01:00
7f4b95c8c4 Removed files. 2017-11-04 19:35:06 +01:00
0e4f96640c Ommited to entire conf folder in api. 2017-11-04 19:34:28 +01:00
dc685ebcdd Removed conf files from api. 2017-11-04 19:30:57 +01:00
6e9d511b5c Added empty log file and stripped config file. 2017-11-04 19:28:34 +01:00
2fc9674548 Added some todos for routers/middleware and setup so that raven will be used for error handling and gets value from config file. 2017-11-04 18:11:44 +01:00
38383da1b9 Merge pull request #61 from KevinMidboe/sentry
Setup for sentry.
2017-10-26 15:20:31 +02:00
7cce61b3bb Setup for sentry. Need to add __DSN__ value manually. 2017-10-26 15:19:10 +02:00
303ec8fa10 Merge pull request #60 from KevinMidboe/lint
Lint
2017-10-26 14:53:06 +02:00
e417fedece Now follows our airbnb linting schema. 2017-10-26 14:52:00 +02:00
6dd9cf7083 Added eslint and set preset to airbnb base. 2017-10-26 14:51:12 +02:00
546f33883a Merge pull request #59 from KevinMidboe/conf_webpack
Changed to now use a prod and a dev webpack config. Our prod config n…
2017-10-24 23:02:00 +02:00
d10b974545 Changed to now use a prod and a dev webpack config. Our prod config now minifies and sets up caching for us. Package json now has build command. 2017-10-24 23:01:44 +02:00
c6fac11b8d Merge pull request #58 from KevinMidboe/client_styling
Changed the box shadow and margin of result wrapper.
2017-10-23 16:12:57 +02:00
d6c066266c Changed the box shadow and margin of result wrapper. 2017-10-23 16:12:00 +02:00
5c408b826b Merge pull request #57 from KevinMidboe/client_title-position
Changed the title position of our result list.
2017-10-23 16:07:06 +02:00
a0ce4e07fe Changed the title position of our result list. 2017-10-23 16:06:38 +02:00
1b58cb461c Merge pull request #56 from KevinMidboe/client_show-request-user
Now also prints the requestee of a element if it is not null.
2017-10-23 15:50:31 +02:00
dbd258d9fa Now also prints the requestee of a element if it is not null. 2017-10-23 15:49:54 +02:00
1bdf89b69f Merge pull request #55 from KevinMidboe/api_tuning
Lowered the tollerance for what objects we choose to look at by tmdb.…
2017-10-23 15:39:57 +02:00
498bd13a1f Lowered the tollerance for what objects we choose to look at by tmdb. That is we only accept items that have some value of vote or popularity. 2017-10-23 15:39:28 +02:00
80dcba2fba Merge pull request #54 from KevinMidboe/api_save-user-w-request
Api save user w request
2017-10-23 15:36:28 +02:00
effa41978e Merge pull request #53 from KevinMidboe/api_search-tmdb-list
Api search tmdb list
2017-10-23 15:35:35 +02:00
7069db8901 Now checks for loggedinuser header variable and passes it to sendRequest. 2017-10-23 15:32:49 +02:00
81fbc86cad Now loggedinuser cookie variable is sent in our header on each call. 2017-10-23 15:31:41 +02:00
6f12d0ca49 Added docstring to new functions in tmdb 2017-10-23 15:14:16 +02:00
2d986eb5b3 Removed unused controllers 2017-10-23 15:06:22 +02:00
233ad03dd3 Changed so our client now uses our new endpoint for searching lists. 2017-10-23 15:05:42 +02:00
40928a6e46 Added docstring 2017-10-23 14:59:58 +02:00
c48db517ac Created one function for searching for different tmdb lists. This replaces 4 endpoints so we have a single way of searching for lists. 2017-10-23 14:58:52 +02:00
d836dd01d3 Merge pull request #52 from KevinMidboe/app/search-the-bay
App/search the bay
2017-10-22 18:56:00 +02:00
da0609d56c Also passes our regex if there is a year now. 2017-10-22 18:54:38 +02:00
f15df29801 More general re matching for date and also returns empty dict of no candidates are found. 2017-10-22 18:50:50 +02:00
4b01387e56 Merge pull request #51 from KevinMidboe/api_cache
Api cache
2017-10-22 18:19:36 +02:00
0e3189ff11 Added default cache time to be 1 day 2017-10-22 18:16:39 +02:00
1925c2a5e3 Changed the variable for saving to searchHistory to be set when user is not false. 2017-10-22 18:04:20 +02:00
7952e6015a Added caching for all tmdb requests. 2017-10-22 17:41:12 +02:00
6e95d48f11 Now when searching a loggedinuser header element is passed also. 2017-10-22 17:34:16 +02:00
d2d791f5b2 added loggedinuser as acceptable header. 2017-10-22 17:31:37 +02:00
0e70a10cc2 Opps, start with then. 2017-10-22 17:31:14 +02:00
592528dec2 Forgot to change this to point at our new server 2017-10-22 17:30:49 +02:00
354e06a963 Removed searchHistory from tmdb class. 2017-10-22 17:03:40 +02:00
cc8d9f0c50 Added docstring, removed a unused console.log and added better error handling for empty return statement.s 2017-10-22 17:03:16 +02:00
8d248135e0 Chagned from using user.username, to just the variable user. 2017-10-22 17:01:58 +02:00
6574c087bc Added user history saving and that tmdb now gets to use cache. 2017-10-22 17:01:26 +02:00
d90197b8b4 Added a cookie variable loggedInUser, to see search history of a user. 2017-10-22 14:58:38 +02:00
d20ac6470f Was a missing ) 2017-10-22 14:18:38 +02:00
8f4e3a24f4 And the last that imports tmdb class. 2017-10-22 14:16:43 +02:00
f89d81681a Added import for cache to initiate our tmdb class in the correct way, but still not implemented the actual cache lookup. 2017-10-22 14:14:29 +02:00
1b29a08ce8 Removed a unused comment 2017-10-22 14:10:25 +02:00
f57878813d Added Cache file for tmdb queries and controller check the cache and returns it if hit. This is to limit our request to a third party library. 2017-10-22 14:09:57 +02:00
b82efddeba Now tmdb searches also check cache for a matching query request, if so returns it. 2017-10-22 14:08:36 +02:00
6ec6fc771a Merge pull request #49 from KevinMidboe/pirate_bugfixes
Pirate bugfixes
2017-10-21 16:13:36 +02:00
de33f02aeb Temp here just to be able to add torrents. 2017-10-21 16:12:49 +02:00
7d4178a901 Now uses python2, not python3 because of libtorrent library. 2017-10-21 16:12:27 +02:00
e5b3c8a249 Changed the logging dir to be based on __file__ location. 2017-10-21 16:11:36 +02:00
1183126653 Merge pull request #48 from KevinMidboe/api_add-magnet
Api add magnet
2017-10-21 15:39:15 +02:00
dbcb316bfd Merge pull request #47 from KevinMidboe/client_search-the-bay
Client search the bay
2017-10-21 15:38:31 +02:00
b6b9c790e9 Errors. Removed a return from within a resolve and a stray { in addMagnet. 2017-10-21 15:33:40 +02:00
1425da3090 Missed to add some extra parantheses around our return statement. 2017-10-21 15:31:04 +02:00
d1d204691a Changed from GET to POST 2017-10-21 15:28:32 +02:00
5a24837163 Controller for adding magnets from post body magnet variable. 2017-10-21 15:28:13 +02:00
4c7be51a61 Added endpoint for add and now user must be authenticated for both pirate endpoints. 2017-10-21 15:27:21 +02:00
3e6fc0960c PirateRepo now has a function for calling our add magnet python script with a magnet passed from the controller. 2017-10-21 15:26:29 +02:00
3d73000e92 Displays a button that can be used to fetch search results and then each element has a magnet button to send it back. 2017-10-21 15:16:58 +02:00
71e7f46927 AdminRequestInfo is also passed listItemSelected variable 2017-10-21 15:16:15 +02:00
5d40bd8e06 Added Pirate Search component. 2017-10-21 15:15:28 +02:00
799b8731c5 Merge pull request #46 from KevinMidboe/api/update_requested
Api/update requested
2017-10-21 12:46:32 +02:00
74ea05cf17 Fixed error where the was an extra ' character. 2017-10-21 12:45:31 +02:00
db226d6c95 Now uses a input_dir variable that is can be different than the destination. 2017-10-21 12:44:45 +02:00
9b1b041ef5 Now uses ip for other plex server. 2017-10-21 12:43:43 +02:00
3223e423b2 Now uses ip for other plex server 2017-10-21 12:43:06 +02:00
99eb5512df Merge pull request #45 from KevinMidboe/app_pirate
Added our scripts needed for pirate search, these are files that need…
2017-10-21 12:40:38 +02:00
63ba10bc5a Added our scripts needed for pirate search, these are files that need to be imported from the currently in-dev project seasonedParser 2017-10-21 12:39:20 +02:00
3c039447f5 Renamed the endpoint to search. 2017-10-21 12:29:28 +02:00
a6d00714d6 Inputs a query, page and type and will search the bay for the query and return it as a json objec.t 2017-10-21 12:28:21 +02:00
67f5cef718 Now makes a async call for python script and returns the ouput of the script when finished as a json object. 2017-10-21 12:27:39 +02:00
67d343e446 Added endpoint for searching the bay 2017-10-21 10:01:16 +02:00
1f6130a53d Added pirateRepository 2017-10-21 10:00:45 +02:00
f0fb791e9d Removed unused imports 2017-10-21 10:00:13 +02:00
87babed265 Merge pull request #44 from KevinMidboe/client/admin-page
Client/admin page
2017-10-21 09:50:32 +02:00
a0565f79d2 Changed name of variable element to requestElement and now the link name of a list item is generated by generateListElements and if this element is the one selected make it visually different 2017-10-21 09:48:13 +02:00
277105e6df Our selected Request is passed through props, the prop name is now changed from display to selectedRequest. 2017-10-21 09:45:46 +02:00
87a2de1d0b Reflects the changes to the url paramater for admin, is now names requestParam. Also the param is passed to Sidebar so that we can highlight the one selected. 2017-10-21 09:44:12 +02:00
ebfb61d776 Renamed admin param to request from search 2017-10-21 08:22:02 +02:00
4cdf368d2c Merge pull request #43 from KevinMidboe/client/admin_page
Client/admin page
2017-10-21 00:16:01 +02:00
5aaf0ddf84 Moved Admin and LoginForm to admin folder and deleted app and main in replacement for root. 2017-10-21 00:14:50 +02:00
39e363dbf3 Works together with AdminRequestInfo to display info about requested items on the admin page. 2017-10-21 00:12:44 +02:00
7b32dfa35a Moved loginform to admin folder 2017-10-21 00:12:09 +02:00
cd58a830b5 Together with sidebar this loads the info of the selected request item on the admin page. 2017-10-21 00:11:53 +02:00
d9f432a8de Our new router, which is being directly loaded from index.js 2017-10-21 00:11:24 +02:00
2a09f67248 Now index.js uses root instead of app component. 2017-10-21 00:10:51 +02:00
65c1f7ad96 Changed from compDidMount to WillMount so it loads before our render function. 2017-10-21 00:09:29 +02:00
2fd515d997 Fixed a bug where the json object was not being returned and fixed indentation of fetchJSON function. 2017-10-21 00:08:35 +02:00
4297505861 Moved admin to a admin folder and this is now the landing page for all that admin things we want to do. It now just displays the sidebar and adminRequestInfo for a selected item in the sidebar. Also checks if user is logged in before loading view. 2017-10-21 00:06:50 +02:00
5bd1041deb Merge pull request #42 from KevinMidboe/client/update_requested
Client/update requested
2017-10-20 18:13:30 +02:00
e114cff32f This will be handling all http requests. 2017-10-20 18:11:34 +02:00
cdba914758 Where all our button components will recide. 2017-10-20 18:11:15 +02:00
5f13ad202c Added import of fetchJSON function in http 2017-10-20 18:10:53 +02:00
8fa7f7f918 Just added a justify content variable 2017-10-20 18:09:43 +02:00
b2f4151850 Movie object now uses http functions when submitting a post for a movie. 2017-10-20 18:08:31 +02:00
45a84cbf85 The admin page that has all the fetched elements now prints all the elements out in a grid. Tried a cool thing on how to render the dropdown for changing the status of a element, but its not working atm. 2017-10-20 17:20:43 +02:00
42d0b40825 Does not fit on this branch, but updated the key value, so it is concat of both id and index of the map function for search results. It was prev just the index, and when rapped this hits same value, which is unacceptable by react. 2017-10-07 14:52:49 +02:00
d9a22f506e When posting a request, the type is also added to the database. 2017-10-06 15:38:22 +02:00
307a8352a3 Now takes both id and type because there is two different sets of id pools, and need to select the corrent one. 2017-10-06 15:37:09 +02:00
3c372aab92 Added returnstatement to updateRequestedById and fixed the sql query. 2017-10-06 15:19:08 +02:00
650c2603e4 Changed the input path of requestRepo 2017-10-06 15:16:45 +02:00
fb479e7a37 Renamed function in requestRepo to updateRequestedById 2017-10-06 15:14:31 +02:00
ac38dacedd Changed database table name. 2017-10-06 15:04:50 +02:00
f54bd0f743 Added a function for updating the status from a given id in requests database. 2017-10-06 15:04:18 +02:00
61c242b5eb Passes the id from request parameter and status we want to change to from the request body. 2017-10-06 15:03:47 +02:00
51d446f378 Added endpoint for updating a requested item. 2017-10-06 15:02:44 +02:00
9b6e7af3ee Merge pull request #41 from KevinMidboe/client/admin
Client/admin
2017-10-06 12:28:05 +02:00
42749c5b64 Now searchRequest passes a index key to movieObject so that each div object can have a separate key. 2017-10-06 12:20:51 +02:00
2e47324005 Merge pull request #40 from KevinMidboe/fix/api
Fix/api
2017-10-06 12:15:08 +02:00
f8ff71bcff Removed a no longer used ip address for allowed origins. 2017-10-06 12:14:05 +02:00
7e27e19a0d Added more strict header allowence and was a error where endpoints for user was not using router, but app. 2017-10-06 12:12:21 +02:00
c954bd4874 Added stylesheet for requestElement 2017-10-06 12:08:06 +02:00
dbb8eb4057 Added redux reducer and store for calling from the login page. 2017-10-06 12:07:43 +02:00
c03449b9e9 The login form for authentication a user. 2017-10-06 12:05:54 +02:00
6dd45cf89e Empty header file. 2017-10-06 12:05:00 +02:00
3bc8c5912b Loader for what pages is to be displayed based on the state of cookie: logged_in. Either displays login page or fetchrequested. 2017-10-06 12:04:35 +02:00
bd74e1dee1 The first thing the index does is to load the app file in a hashrouter object and send it to the element with id root. 2017-10-06 12:03:16 +02:00
ed1d0f3c39 App.jsx now loads the header and the main script that holds our routes. 2017-10-06 12:01:30 +02:00
0b79c8679d This is the landing page for viewing all requested items. One must be authenticated through the admin page, and requests need the token variable in browser storage. The returned data can be filtered and sorted. 2017-10-06 12:00:51 +02:00
927352e9ae Added react-redux, react-router and other redux packages. 2017-10-06 11:59:16 +02:00
5b110b9d82 This is the main router for index page, routes to searchRequest and admin pages. 2017-10-06 11:57:22 +02:00
1633be1276 Added 404 page when a request does not match any routes. 2017-10-06 11:56:22 +02:00
6d5c1e8d6d File that holds functions for retriveing and saving cookie data. 2017-10-06 11:55:20 +02:00
5e50d52344 Now gitignore is working for new prefs. 2017-10-04 14:25:05 +02:00
26c1bda3df Now when calling moveSeason python script, the python version used is python3. 2017-10-04 14:18:42 +02:00
da83b73732 Added a parent_input that can be used for constructing the move path to a different path than the destination path. Also moved from using os.move to shutil.move for cross filesystem support. Also removed owner renaming. 2017-10-04 14:07:23 +02:00
d9d8d6ab58 Merge pull request #39 from KevinMidboe/feature/api_request
Feature/api request
2017-10-03 16:28:39 +02:00
5fbedeb4b2 Misspelled User-Agent, when should have been user-agent. 2017-10-03 16:26:03 +02:00
455bf565b2 Moved the ? marker in the sql query to the end of the statement. 2017-10-03 16:19:55 +02:00
a819a4bbf3 Now passes the user agent that is making the post request (requesting content) so it can be saved in the db. 2017-10-03 16:11:15 +02:00
f50e0b2a59 Added user_agent to be passed to and saved in db when a request is made. 2017-10-03 16:10:27 +02:00
9d2c7c4c2f Added variable for status for database request query. 2017-10-03 16:01:28 +02:00
8908e36e89 Merge branch 'master' of github.com:KevinMidboe/seasonedShows 2017-09-27 21:55:33 +02:00
33aefef362 This was for some reason removed, now added again. 2017-09-27 21:55:21 +02:00
cee27fd9d2 Merge pull request #38 from KevinMidboe/bugfix/infolinkForShows
Added a check for the type and then change the info link to end with … #35
2017-09-27 20:43:54 +02:00
2bacc54687 Added a check for the type and then change the info link to end with either movie or tv. 2017-09-27 20:41:49 +02:00
850401fd7a Merge pull request #37 from KevinMidboe/feature/getRequested
Feature/get requested
2017-09-27 17:58:16 +02:00
a8166bcad9 Removed unused console. 2017-09-27 17:56:36 +02:00
149b05ef28 Calling function for fetching all the requested items. 2017-09-27 17:55:07 +02:00
e43bcbdd52 Added endpoint for getting all requests. 2017-09-27 17:54:34 +02:00
509759bca8 Added a function in requestRepo to fetch out all the requested items. And a sql statement to do so. 2017-09-27 17:54:01 +02:00
f1f407c71c Merge pull request #36 from KevinMidboe/feature/authentication
Feature/authentication
2017-09-27 16:30:05 +02:00
bab4af08d9 Added the use of tokenToUser so that it is verified that a valid token is in the request header when doing actions that need login verification. 2017-09-27 16:27:05 +02:00
a3de70e2da Created a middleware for requests that checks for a token in the Authentication field in the header and verifies that the token is valid for a user. 2017-09-27 16:25:54 +02:00
698d2d6072 Created endpoints for user tasks like login, register and a test endpoint history. A test for checking that session token works as expected. 2017-09-27 16:19:02 +02:00
6ec6c7f9cb Added token, user, userRepository and userSecurity in user folder. This handles creating new users, loging in and creating a user specific token then returning it when logging in. 2017-09-27 16:15:28 +02:00
72654fd465 Added searchHistory for adding a logged in users history trace. (More like a test function of the page) 2017-09-27 16:13:39 +02:00
f4aee549be Added bcrypt and jsonwebtoken to package list 2017-09-27 16:12:20 +02:00
bb6dd1ad59 Added middleware for authentiaction, endpoints for a user and import for token handling. 2017-09-27 14:02:59 +02:00
a14d9e5ec6 Merge pull request #34 from KevinMidboe/update/frontend_logic
Update/frontend logic
2017-09-27 14:00:29 +02:00
5ceb19f449 Merge branch 'update/frontend_logic' of github.com:kevinmidboe/seasonedShows into update/frontend_logic 2017-09-27 13:51:07 +02:00
8b5b8bb0b7 Reduced the filter values to return more results. 2017-09-27 13:49:49 +02:00
6b3fe8c46f Merge pull request #33 from KevinMidboe/update/frontend_logic
Update/frontend logic
2017-09-27 00:47:25 +02:00
98dafe56c4 Merge branch 'master' into update/frontend_logic 2017-09-27 00:45:49 +02:00
44d8dbfe0c Updated gitignore to include yarn.locks. 2017-09-27 00:30:51 +02:00
6d0a91dc93 Changed the default filter values to be multi until a filter selector is implemented. 2017-09-27 00:29:28 +02:00
8014f01766 Added stricter handling for selecting what mediaType to convert to when searching tmdb and converting to seasoned object. 2017-09-27 00:28:41 +02:00
d787fba024 Changed a smellingerror (reponse -> response). Also now pass the type of the search request when converting to seasoned object. Fixed issue where number_of_items_on_page was not set to the length of the list. 2017-09-27 00:22:22 +02:00
6bd1b29d5e Changed the font size of the header, space between the search bar and result content. Also changed so that the font size in the search bar is large enough to not zoom on ios because of smaller than standard font size. 2017-09-27 00:19:19 +02:00
86c479de15 Changed so that the output is split in two idependent mediaQuery items at the second most parent. This is a lazy way to controll all elements of when resizing to a smaller screen. 2017-09-27 00:18:06 +02:00
daa9a7749e Now also the ip address is passed to the sendRequest function in requestRepository so that the requesters ip address can be logged in the database. 2017-09-26 20:43:56 +02:00
d47f2bf757 Added support for database operations. Now when requesting a item, it is saved to a database and sends a email from pi.midboe account. 2017-09-26 20:42:49 +02:00
6f54a61223 Removed a extra / that was in the posterURL 2017-09-26 20:41:25 +02:00
1321671840 Added infinite-scroller, notify-toast, urijs. Also burger-menu for later support for selecting discover lists. 2017-09-26 20:40:11 +02:00
1af9368a6c Updated stylesheets to reflect changes to request and movieObject pages. 2017-09-26 20:38:13 +02:00
5341e940c6 Added viewport width to header of main index.html page. 2017-09-26 20:37:24 +02:00
30226af6f6 Lots of style changes done to look neater. Now when scrolled to bottom of the page it fetches more items from the api. 2017-09-26 20:36:37 +02:00
b2f9d6f5f5 Added rating and background to the class constructor. Added type variable to url when requesting a movie/show and added a notification agent. Now this page is also updated to support mobile formatting, when tilted it shows the background image instead of poster image. 2017-09-26 20:34:54 +02:00
6b4daf140a Change a grammar error 2017-09-21 16:58:44 +02:00
a5d270967e Merge pull request #31 from KevinMidboe/fix/seasonedApiSearch
Fix/seasoned api search
2017-09-21 16:39:00 +02:00
e281e73293 Now the type is checked and passed to convertTmdbToSeasoned if it is movie or show and not multiwq 2017-09-21 16:37:53 +02:00
743c132aef If a strict type is passed to convertTmdbToSeasoned than this is now used to set the type of objects to be made. 2017-09-21 16:37:10 +02:00
b219242787 Added infinate scroll to lists also. 2017-09-21 15:04:53 +02:00
00d000b8f8 Started working in the infinate scroll so it loads the next page when visible on page. This commit was under_development bugs as of now. 2017-09-21 15:03:33 +02:00
c79d5dbc6e Also rewrote the funtionality for fetching lists from tmdb. It is now done in a similar fashion as searching requests, but with its own error handling and messaging. 2017-09-21 15:01:51 +02:00
372ec1b241 Rewrote most of how api calls are made when searching for a movie and how the returning data is handled. We now have finer handling of status response from the original api call aswell as if any of the functions hit a error e.g. not hitting the server, than we have our own errors thrown. Also updated the page incrementers to updated the last api call with a higher or lower number. 2017-09-21 14:36:41 +02:00
4c2982293b Merge pull request #29 from KevinMidboe/searchRequest
Search request
2017-09-20 10:49:27 +02:00
072f0cca93 Forgot to append the tmdb endpoint to the baseurl in searchRequest 2017-09-20 10:46:10 +02:00
acd0a1782d Changed the links from localhost to the correct global url 2017-09-20 10:38:10 +02:00
d9dd7ec153 It is now expected to get a JSON object were the items we want to display are listed in results object within, this is now reflected in the client by iterating over data.results not just data. 2017-09-20 10:26:30 +02:00
ae66e9a684 Changed variable name from movie to searchResult and now checks if the result list in the return JSON object is empty before sending a result back to client. 2017-09-20 10:25:01 +02:00
2d465d841e Before when doing a request search we had a lot of promisses and loops within one-another, now it's more split so easier to read and better error handling if something goes wrong when looking up either in plex or tmdb api. 2017-09-20 10:23:36 +02:00
72d50209a5 Rewrote the search function in tmdb to now return a JSON object with results: (all movie/show objects), number_of_items: (count of result objects), page and total page. Split up the filtering and converting of the result from tmdb api to finished seasoned objects for better readability. 2017-09-20 10:21:57 +02:00
fae3fcc74c Merge branch 'master' of github.com:KevinMidboe/seasonedShows 2017-09-12 20:15:41 +02:00
12835d4ed5 Changed the location of moveseasoned app script. 2017-09-12 20:15:20 +02:00
c187ce7515 Merge pull request #26 from KevinMidboe/client_features
Now we can fetch lists for discover, popular, now playing and upcomin…
2017-09-12 14:40:46 +02:00
4fa32440af Now we can fetch lists for discover, popular, now playing and upcoming with optional tv_show and movie filter options. 2017-09-12 14:39:32 +02:00
cf90616ec9 Set theme jekyll-theme-cayman 2017-09-08 20:22:54 +02:00
895d52b41a Set theme jekyll-theme-cayman 2017-09-08 20:22:53 +02:00
d26f4b0531 Set theme jekyll-theme-cayman 2017-09-08 20:21:56 +02:00
d347a2bbfe Merge pull request #25 from KevinMidboe/documentation
Documentation
2017-09-08 12:49:49 +02:00
8e894b3dfb Merge pull request #23 from KevinMidboe/bugfix_api/plex/playing
Bugfix api/plex/playing #21
2017-09-07 23:57:41 +02:00
fa3d56c4e5 Merge pull request #22 from KevinMidboe/client_features
Client features
2017-09-07 23:56:02 +02:00
eb2de340a3 Added variables for audioProfile, videoProfile, duration and container for a playback. 2017-09-07 23:52:05 +02:00
0370f051bc Changed the error message returned if something goes wrong when fetching playing 2017-09-07 23:51:22 +02:00
3eb2bbe54e Removed a console log 2017-09-07 23:49:21 +02:00
a8c6850863 For some reason this was player info, but it is now been replaced with all the variables specific for mediaInfo 2017-09-07 23:48:06 +02:00
cd52d295b0 Now both Media and Media.Part in the JSON object about a stream is a list and need to index the first element of the stream to get this info. 2017-09-07 23:47:11 +02:00
e9c0e5bfa6 Removed extra space under header 2017-09-04 13:45:18 +02:00
fcf6b324c9 Moved the pitch sentence down under the header 2017-09-04 13:44:34 +02:00
231b70c226 Added what the different parts of seasoned does, that is external, api and services. 2017-09-04 13:43:31 +02:00
28d0b63960 Added TODO and a output of what is requested in the router 2017-09-03 18:17:07 +02:00
9eb31ea433 Added type input to the sendRequest function. 2017-09-03 18:16:26 +02:00
2b7f9551bf Moved styles from serachRequest to a seapate file in ./styles 2017-09-03 17:34:45 +02:00
6a4e2bc2ab Moved all styling to a separate file in ./style 2017-09-03 17:29:51 +02:00
850452db78 Removed unnesessary this.id when calling function, this is already a object, don't need to pass itself. 2017-09-03 17:07:53 +02:00
7a1f709d90 Small changed that first checks if the variable is undefined or not and then length 2017-09-03 17:06:19 +02:00
e81f5c8057 Added functions for discover, upcoming, nowplaying, popular and similar. Also documented better and some finer error handling with more logical responses. 2017-09-03 17:04:59 +02:00
789ed77ab6 Now the type of object being passed can be set by strictType variable. The type is then set before creating a movie or show object item. 2017-09-03 17:03:31 +02:00
2dc22b386d Added endpoints for discover media (either movie or show), getUpcoming movies, nowPlaying (movies or shows), popular (movie or show) and search similar where the type movie/show must be defined 2017-09-03 17:02:02 +02:00
3e12cf10fb Updated tmdb to use the new endpoints 2017-09-03 16:59:52 +02:00
c64f6a861e Merge branch 'master' of github.com:kevinmidboe/seasonedShows into client_features 2017-09-02 23:16:46 +02:00
70b284380f Removed unused console.log 2017-09-02 23:16:24 +02:00
e33e7d8dc7 Removed unsued imports. 2017-09-02 23:15:30 +02:00
a5bb42f175 Merge pull request #20 from KevinMidboe/api_bugfixing
Api bugfixing and changed value of filter for tmdb search
2017-09-02 22:54:09 +02:00
dacd44a8f5 Changed the value of the filter to be vote_count >= 80 or popularity > 18 2017-09-02 22:53:04 +02:00
84a897da6e Added popularity and vote_count to movie and show object and added a new check for name and title of movie 2017-09-02 22:51:49 +02:00
d5479be563 Merge pull request #19 from KevinMidboe/api_bugfixing
Api bugfixing
2017-09-02 21:15:36 +02:00
849bc4aa5e Merge pull request #18 from KevinMidboe/app_style_update
App style update
2017-09-02 21:13:43 +02:00
17dd66e9ac Fixed a bug where if a seach query did not pass anything back when checking existance in plex, then you would get a 404 message. 2017-09-02 21:12:38 +02:00
8149c8f732 Added date_added for plex added date. 2017-09-02 20:50:19 +02:00
8652a0ee0f Rewrote for more general use. 2017-09-02 20:49:50 +02:00
101dc7d570 Changed the filter cutoff to be based on, if the show/movie has popularity over 2, vote_count over 20 and has a defined release_date/first_air_date. Also changed so that it now uses the new convertTmdbToSeaonsed insted of the old (convertTmdbToMovie) 2017-09-02 20:49:09 +02:00
3bf646483d Renamed file. Now converts to both movie and show. Also added more data that is saved to the object. 2017-09-02 18:13:51 +02:00
622e89ea50 Changed import name from convertPlexToMovie -> convertPlexToSeasoned and changed from map function to reduce to be able to filter only movies and shows to be parsed. 2017-09-02 18:13:08 +02:00
b4a005da42 Changed the name of convertPlexToMovie -> convertPlexToSeasoned 2017-09-02 18:11:59 +02:00
1a858d09bb Created a new shows class that only holds relevant information for shows. 2017-09-02 18:10:16 +02:00
a4fe5cc635 Made this a singular movie object that now only holds relevant information for movies. 2017-09-02 18:09:43 +02:00
a82dc1ae78 Added requestRep again 2017-09-02 16:36:54 +02:00
ebe09390d2 Removed unused css loaders 2017-09-02 16:32:57 +02:00
d4fdf6bdcf Updated babel node packages 2017-09-02 16:32:35 +02:00
72c4a43d2e Also has a lot of inline css, but not the search type buttons have function, but not feedback. 2017-09-02 16:30:42 +02:00
d77a4c6d9e Now it is a little messy with lots of css inline, but will clean and comment. Has now a working UI, needs more 404 handling of items. 2017-09-02 16:30:02 +02:00
e33840f1db Commented out unused elements. 2017-09-02 16:03:43 +02:00
bd3d8f385b Added link to font-awesome and google font api. Also set the margin: 0 for body. 2017-08-31 13:16:06 +02:00
5706c02a5d Updated much of the surrounding styling for the request page, still need to ble cleaned. 2017-08-31 13:14:57 +02:00
ee019f5674 Now with new styling, still need to do cleanup, but added a big bulk of the grutt. 2017-08-31 13:13:50 +02:00
56405e54f9 Downgraded from sqlite3 to sqlite because of connection function error. 2017-08-23 10:26:23 +02:00
daa1915203 Merge pull request #17 from KevinMidboe/filestructure_cleanup
Filestructure cleanup
2017-08-14 22:50:27 +02:00
ea97a0b4af Moved all python related scripts to app directory 2017-08-14 22:42:48 +02:00
d2bed84ecb Moved conf folder to seasoned_api 2017-08-14 22:42:24 +02:00
b8d01fcf1c Changed filename for moveSeasoned to include dir 2017-08-14 22:33:28 +02:00
47aa638fd8 Moved eveything related to the api to a seperate folder seasoned_api. 2017-08-06 16:45:02 +02:00
f53ab55d96 Merge pull request #16 from KevinMidboe/request_ui
Request ui
2017-08-06 09:26:22 +02:00
20a7cb0ba1 Added yarn lock files 2017-08-06 09:23:48 +02:00
10da9874fc Added yarn error log 2017-08-06 09:22:26 +02:00
bc4eff795e Added quotes to poster url 2017-08-06 09:19:39 +02:00
82add07e00 Font that is not yet in use 2017-08-05 17:27:01 +02:00
e5cb80afc8 Mostly unused style file 2017-08-05 17:26:40 +02:00
891b7ecb28 Added CSS loaders to webpack.config 2017-08-05 17:24:08 +02:00
292922c06c Added webfontloader and css-loader to package.json 2017-08-05 17:23:45 +02:00
9265b469c4 Added style to the elements of the html elements 2017-08-05 17:06:26 +02:00
c892755759 Created function for creating a HTML object that is returned to the caller. 2017-08-05 17:05:15 +02:00
d2c456305a Added style to background and main object 2017-08-05 17:04:44 +02:00
66b9c8c3db Updated webpack version 2017-08-05 17:02:38 +02:00
fbf49e4cd9 Updated to use sqlite3 instead of sqlite in api 2017-07-29 13:21:25 +02:00
a436f79770 Merge pull request #15 from KevinMidboe/submit_request
Submit request
2017-07-17 17:17:38 +02:00
6b5a2341bf Class file that creates and returns objects for the mail sender in a single place. 2017-07-16 13:47:49 +02:00
30d26d82c3 Added nodemailer to send email of the requested movie 2017-07-16 13:47:06 +02:00
a6af5e8c5d Now returns just id of the movie when clicked button. 2017-07-16 13:45:42 +02:00
6322b0b17b Formatting changes, sorted alpha 2017-07-16 13:45:05 +02:00
e77a05db07 Merge pull request #14 from KevinMidboe/client_feature
Client feature
2017-07-16 11:19:15 +02:00
f2cc05307b Added TODO 2017-07-16 11:18:14 +02:00
90546755cc Rewrote much of the fetching of media items for the rendered request page. 2017-07-16 11:11:43 +02:00
b34e23077b Updated gitignore with dist folder 2017-07-16 11:08:36 +02:00
c45df8a131 Added controller for calling POST request function for media items. 2017-07-16 11:08:14 +02:00
91606fc9b8 Removed unused import 2017-07-16 11:05:30 +02:00
3047dce147 Opened the POST endpoint for requesting a movie in the server 2017-07-16 11:04:56 +02:00
Kevin Midbøe
2c97803d82 Added a sendRequest function for handliing the post of a movie request 2017-07-16 11:03:30 +02:00
Kevin Midbøe
97dea47a7a Updated inherits in yarn config file 2017-07-16 10:54:44 +02:00
Kevin Midbøe
c97ab972ef Changed movieObject ot pass full object, not just it's id when clicked 2017-07-16 10:51:02 +02:00
Kevin Midbøe
29e575cbf1 Start of fire script to move bulk files to wanted dir, while creating the respected folder names. 2017-07-12 00:11:05 +02:00
b2848e6a7e Merge pull request #13 from KevinMidboe/api
Verify that respond has content, if so send the content back. If no c…
2017-07-01 08:47:19 +02:00
1663f5931d Verify that respond has content, if so send the content back. If no content was found, send 404 status and error message 2017-07-01 08:46:37 +02:00
5b8394c5a0 Merge pull request #12 from KevinMidboe/api
If search request returns empty array from tmdb, then send a 404 repo…
2017-07-01 08:37:53 +02:00
ba3a1fa028 If search request returns empty array from tmdb, then send a 404 reponse with error message back to client. 2017-07-01 08:37:29 +02:00
b358c61efb Had a bug that when searched and empty return it tried to map the empty array. Now we check for the contents of the response before we map the items. 2017-07-01 08:29:49 +02:00
71130e66f3 Merge pull request #11 from KevinMidboe/rename_and_move
Rename and move
2017-06-28 00:03:13 +02:00
82d2e439fe Merge branch 'master' into rename_and_move 2017-06-28 00:03:04 +02:00
be839ba2dd Trying to fix bug with os.join, now has a two args, not list 2017-06-27 15:58:40 -06:00
f884406c06 Missed a syntax errro 2017-06-27 15:56:47 -06:00
04066b8da4 Now the full path is sent to fix ownership, not just the file path 2017-06-27 15:56:06 -06:00
34ab8be097 Fix_ownership got a list of files, now it iterates through this list and sends each item to the function 2017-06-27 15:53:52 -06:00
31e16e2784 Now the for loop actually goes through dir not just the string of file name. *facepalm* 2017-06-27 15:52:17 -06:00
7cda4accdb Added print for debugging purposes 2017-06-27 15:49:34 -06:00
7023b135b4 Now all subfiles also have their permission chagned. 2017-06-27 15:48:25 -06:00
979a95a468 Added a try except to remove and a todo to remind of need improvements. 2017-06-27 15:37:55 -06:00
c1cd821d8a Changed group id to the wanted value. 2017-06-27 15:33:50 -06:00
4b54339b72 Opps, changed to use the int value of uid and gid, not looking up the int value and then converting to int. Does not make sense. 2017-06-27 15:30:34 -06:00
3d8dc80bb9 Merge pull request #10 from KevinMidboe/subtitles
Subtitles
2017-06-27 23:27:07 +02:00
8a53cc4765 Added todo for fix_ownership 2017-06-27 15:25:19 -06:00
3bc539323a Changed user and group id to the wanted value. 2017-06-27 15:24:49 -06:00
80746252c0 Added excepts to all move and delete, so it can be run mulitple times and see no downside to having it run and logging it if not found. Also think I fixed a bug that renamed the folder that would be deleted, not the new folder 2017-06-27 15:22:02 -06:00
d5ea7a6bbb Error with function call 2017-06-18 21:49:58 -06:00
17b89748e1 Added todo to analyseSubtitles function 2017-06-18 21:48:08 -06:00
5a45856699 Pulled variable for subtitles path out of open() and retunes subfile without analysis on typeError. 2017-06-18 21:47:15 -06:00
90384e4ebc Calls fix_ownership after creating new directory 2017-06-13 05:17:40 +02:00
7f14f64762 Set gid and uid to static vars 2017-06-13 05:11:50 +02:00
2333559477 Merge branch 'master' of github.com:KevinMidboe/seasonedShows 2017-06-12 20:32:14 +02:00
a6d21ed181 Added fix_ownership for setting the new folder to user, not root. 2017-06-12 20:28:58 +02:00
1eb46a41e0 Merge branch 'master' of github.com:KevinMidboe/seasonedShows 2017-06-04 12:42:01 +02:00
3b5d6fbb6c Merge pull request #9 from KevinMidboe/revert-8-api
Revert "Api"
2017-06-04 00:16:29 +02:00
0022f656bd Revert "Api" 2017-06-04 00:16:16 +02:00
9a02130124 Merge pull request #8 from KevinMidboe/api
Added endpoint for submitting movie/tv requests
2017-06-04 00:16:04 +02:00
3bb43f08f2 Controller for handling submits of item requests. 2017-06-04 00:13:35 +02:00
703e3d3785 Added a api endpoint for submitting a movie request. 2017-06-04 00:13:02 +02:00
6496988e51 Changed variable names to better reflect their purpose 2017-06-04 00:12:38 +02:00
069d984c39 Added for handling input of movie request, but still need to work on the way the python script is run. 2017-06-04 00:10:35 +02:00
27edd29bd2 Started adding features for submitting a request to server 2017-06-04 00:07:23 +02:00
531900744e Merge branch 'master' of github.com:KevinMidboe/seasonedShows 2017-06-03 13:13:23 +02:00
d9cb5b97c7 Merge pull request #7 from KevinMidboe/api
Added overview (description) from tmdb movie to movieObject.
2017-06-03 13:12:32 +02:00
9d29482857 Added overview of requested movie to be printed. 2017-06-03 13:12:10 +02:00
907bc73a7f Added overview (description) from tmdb movie to movieObject. 2017-06-03 13:07:49 +02:00
6f8a7bc06c Merge branch 'master' of github.com:KevinMidboe/seasonedShows 2017-06-03 11:21:15 +02:00
25e4cb1870 Merge pull request #6 from KevinMidboe/api
Changes to header info
2017-06-03 11:19:39 +02:00
b695d1aaf0 Merge pull request #5 from KevinMidboe/client_feature
Client feature
2017-06-03 11:19:01 +02:00
d05aa13c39 Changes to more strict origin policy. 2017-06-03 11:17:09 +02:00
21c40e9b34 Created a function that can be called for updating the mediaList, saving lines! 2017-06-03 11:13:57 +02:00
244f606133 Changed the way html for the object is created, but need to do this with React native functions. Also added a few functions that will be for requesting items. 2017-06-03 11:12:28 +02:00
7152c987b7 Linting changes 2017-06-02 19:39:06 +02:00
47269ebc70 Class for creating a object that can easily be called for the html implicit to the object type 2017-06-02 19:37:58 +02:00
30dd05ebf3 Now the search request updates based on the input in the text field. Super simple, need better handling and styling. 2017-06-02 19:37:08 +02:00
4f2a69f32d Removed a unused log 2017-06-02 19:36:18 +02:00
545152c888 Start of request, now just prints what is written in text field and reads keypress enter 2017-06-02 14:35:43 +02:00
bd3ceb7b6d Added component for getting input for requestSearch. 2017-06-02 14:29:19 +02:00
7c055bd9d1 Created a function for deciding what to return based on the object it computes. 2017-06-02 13:50:42 +02:00
9d11798133 Removed test logging functions 2017-06-02 12:14:19 +02:00
53a60de38b Removed the exit status 2017-06-02 08:54:40 +02:00
74c6ed02a8 Changed debug level and removed a unused exit 2017-06-02 08:52:54 +02:00
237 changed files with 16281 additions and 4385 deletions

15
.gitignore vendored
View File

@@ -1,10 +1,7 @@
__pycache__
nohup.out
shows.db
.DS_Store
env_variables.py
conf/classedOutput.log
node_modules
*.pyc
npm-debug.log
webpage/js/env_variables.js
development.json
env
shows.db
*/package-lock.json

10
.gitmodules vendored Normal file
View File

@@ -0,0 +1,10 @@
# Docs : https://git-scm.com/book/en/v2/Git-Tools-Submodules
[submodule "torrent_search"]
path = torrent_search
url = https://github.com/KevinMidboe/torrent_search.git
branch = master
[submodule "delugeClient"]
path = delugeClient
url = https://github.com/KevinMidboe/delugeClient.git

18
.travis.yml Normal file
View File

@@ -0,0 +1,18 @@
language: node_js
node_js: '11.9.0'
git:
submodules: true
script:
- yarn test
- yarn coverage
before_install:
- cd seasoned_api
before_script:
- yarn
- curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
- chmod +x ./cc-test-reporter
- ./cc-test-reporter before-build
after-script:
- ./cc-test-resporter after-build --exit-code $TRAVIS_TEST_RESULT
cache: false
os: linux

18
ISSUE_TEMPLATE.md Normal file
View File

@@ -0,0 +1,18 @@
## What kind of an issue is this?
- [ ] Bug report
- [ ] Feature request
## Expected behaviour?
## Current behaviour?
*if this is a bug report*
## Steps to reproduce behaviour?
*if this is a bug report*
## Screenshot? 📷
*A image tells a thousands words*

135
README.md
View File

@@ -1,6 +1,137 @@
# *Seasoned*: an intelligent organizer for your shows
*Seasoned* is a intelligent organizer for your tv show episodes. It is made to automate and simplify to process of renaming and moving newly downloaded tv show episodes following Plex file naming and placement.
<h1 align="center">
🌶 seasonedShows
</h1>
<h4 align="center"> Season your media library with the shows and movies that you and your friends want.</h4>
<p align="center">
<a href="https://travis-ci.org/KevinMidboe/seasonedShows">
<img src="https://travis-ci.org/KevinMidboe/seasonedShows.svg?branch=master"
alt="Travis CI">
</a>
<a href="https://coveralls.io/github/KevinMidboe/seasonedShows?branch=api/v2">
<img src="https://coveralls.io/repos/github/KevinMidboe/seasonedShows/badge.svg?branch=api/v2" alt="">
</a>
<a href="https://snyk.io/test/github/KevinMidboe/seasonedShows?targetFile=seasoned_api/package.json">
<img src="https://snyk.io/test/github/KevinMidboe/seasonedShows/badge.svg?targetFile=seasoned_api/package.json" alt="">
</a>
<a href="https://opensource.org/licenses/MIT">
<img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="">
</a>
</p>
<p align="center">
<a href="#demo-documentation">D & D</a>
<a href="#about">About</a>
<a href="#key-features">Key features</a>
<a href="#installation">Installation</a>
<a href="#setup">Setup</a>
<a href="#running">Running</a>
<a href="#daemon">Setup daemon</a>
<a href="#contributing">Contributing</a>
</p>
## <a name="demo-documentation"></a> Demo & Documentation
📺 [DEMO](https://kevinmidboe.com/request)
📝 Documentation of the api.
💖 Checkout my [fancy vue.js page](https://github.com/KevinMidboe/seasonedRequest) for interfacing the api.
## <a name="about"></a> About
This is the backend api for [seasoned request] that allows for uesrs to request movies and shows by fetching movies from themoviedb api and checks them with your plex library to identify if a movie is already present or not. This api allows to search my query, get themoviedb movie lists like popular and now playing, all while checking if the item is already in your plex library. Your friends can create users to see what movies or shows they have requested and searched for.
The api also uses torrent_search to search for matching torrents and returns results from any site or service available from torrent_search. As a admin of the site you can query torrent_search and return a magnet link that can be added to a autoadd folder of your favorite torrent client.
## <a name="key-features"></a> Key features
### Code
- Uses [tmdb api](https://www.themoviedb.org/documentation/api) with over 350k movies and 70k tv shows
- Written asynchronously
- Uses caching for external requests
- Test coverage
- CI and dependency integrated
- Use either config file or env_variables
### Functionality
- Queries plex library to check if items exists
- Create admin and normal user accounts
- [torrent_search](https://github.com/KevinMidboe/torrent_search) to search for torrents
- Fetch curated lists from tmdb
## <a name="installation"></a> Installation
Before we can use seasonedShows we need to download node and a package manager. For instructions on how to install [yarn](https://yarnpkg.com/en/) or [npm](https://www.npmjs.com) package managers refer to [wiki: install package manager](https://github.com/KevinMidboe/seasonedShows/wiki/Install-package-manager). This api is written with express using node.js as the JavaScript runtime engine. To download node.js head over the the official [node.js download page](https://nodejs.org/en/download/).
### Install seasonedShows
After you have downloaded a package manager and node.js javascript engine, the following will guide you through how to download, install and run seasonedShows.
### macOS
- Open terminal
- Install git. This can be done by running `xcode-select --install` in your favorite terminal.
- Install a package manager, refer to this [wiki page] for yarn or [wiki page] for npm
- Type: `git clone --recurse-submodules git@github.com:KevinMidboe/seasonedShows.git`
- Type: `cd seasonedShows/`
- Install required packages
* yarn: `yarn install`
* npm: `npm install`
- Start server:
* yarn: `yarn start`
* npm: `npm run start`
- seasonedShows will now be running at http://localhost:31459
- To have seasonedShows run headless on startup, check out this wiki page to [install as a daemon].
### Linux
- Open terminal
- Install git
* Ubuntu/Debian: `sudo apt-get install git-core`
* Fedora: `sudo yum install git`
- Type: `git clone --recurse-submodules git@github.com:KevinMidboe/seasonedShows.git`
- Type: `cd seasonedShows/`
- Install required packages
* yarn: `yarn install`
* npm: `npm install`
- Start server:
* yarn: `yarn start`
* npm: `npm run start`
- seasonedShows will now be running at http://localhost:31459
- To have seasonedShows run headless on startup, check out this wiki page to [install as a daemon].
-- same --
(install yarn or npm in a different way)
After you have installed the required packages you will have a node_modules directory with all the packages required in packages.json.
### Requirements
- Node 7.6 < [wiki page]
- Plex library
## <a name="setup"></a> Setup and/ configuration
There is a config file template, what the values mean and how to change them.
Also show how to hide file from git if not want to show up as uncommitted file.
Also set variables in environment.
## <a name="running"></a> Running/using
yarn/npm start. (can also say this above)
How to create service on linux. This means that
## <a name="daemon"></a> Setup a daemon
The next step is to setup seasonedShows api to run in the background as a daemon. I have written a [wiki page](https://github.com/KevinMidboe/seasonedShows/wiki/Install-as-a-daemon) on how to create a daemon on several unix distors and macOS.
*Please don't hesitate to add your own system if you get it running on something that is not yet lists on the formentioned wiki page.*
## <a name="contributing"></a> Contributing
- Fork it!
- Create your feature branch: git checkout -b my-new-feature
- Commit your changes: git commit -am 'Add some feature'
- Push to the branch: git push origin my-new-feature
- Submit a pull request
## Api documentation
The goal of this project is to create a full custom stack that can to everything surround downloading, organizing and notifiyng of new media. From the top down we have a website using [tmdb](https://www.themoviedb.com) api to search for from over 350k movies and 70k tv shows. Using [hjone72](https://github.com/hjone72/PlexAuth) great PHP reverse proxy we can have a secure way of allowing users to login with their plex credentials which limits request capabilites to only users that are authenticated to use your plex library.
seasonedShows is a intelligent organizer for your tv show episodes. It is made to automate and simplify to process of renaming and moving newly downloaded tv show episodes following Plex file naming and placement.
So this is a multipart system that lets your plex users request movies, and then from the admin page the owner can.
## Installation
There are two main ways of
## Architecture
The flow of the system will first check for new folders in your tv shows directory, if a new file is found it's contents are analyzed, stored and tweets suggested changes to it's contents to use_admin.

1
_config.yml Normal file
View File

@@ -0,0 +1 @@
theme: jekyll-theme-cayman

108
app/.gitignore vendored Normal file
View File

@@ -0,0 +1,108 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
# Translations
*.mo
*.pot
# Django stuff:
*.log
.static_storage/
.media/
local_settings.py
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# pyenv
.python-version
# celery beat schedule file
celerybeat-schedule
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
# - - - - -
# My own gitignore files and folders
env_variables.py

View File

@@ -3,7 +3,7 @@
# @Author: KevinMidboe
# @Date: 2017-04-05 18:40:11
# @Last Modified by: KevinMidboe
# @Last Modified time: 2017-06-01 19:02:04
# @Last Modified time: 2018-04-03 22:58:20
import os.path, hashlib, time, glob, sqlite3, re, json, tweepy
import logging
from functools import reduce
@@ -61,7 +61,7 @@ class strayEpisode(object):
return hashlib.md5("b'{}'".format(self.parent).encode()).hexdigest()[:8]
def findSeriesName(self):
find = re.compile("^[a-zA-Z. ]*")
find = re.compile("^[a-zA-Z0-9. ]*")
m = re.match(find, self.parent)
if m:
name, hit = process.extractOne(m.group(0), getShowNames().keys())
@@ -91,7 +91,12 @@ class strayEpisode(object):
def analyseSubtitles(self, subFile):
# TODO verify that it is a file
f = open(os.path.join([env.show_dir, self.parent, subFile]), 'r', encoding='ISO-8859-15')
try:
subtitlePath = os.path.join([env.input_dir, self.parent, subFile])
except TypeError:
# TODO don't get a list in subtitlePath
return self.removeUploadSign(subFile)
f = open(subtitlesPath, 'r', encoding='ISO-8859-15')
language = detect(f.read())
f.close()
@@ -126,7 +131,7 @@ class strayEpisode(object):
conn = sqlite3.connect(env.db_path)
c = conn.cursor()
path = '/'.join([env.show_dir, self.parent])
path = '/'.join([env.input_dir, self.parent])
video_files = json.dumps(self.videoFiles)
subtitles = json.dumps(self.subtitles)
trash = json.dumps(self.trash)
@@ -144,14 +149,13 @@ class strayEpisode(object):
def getDirContent(dir=env.show_dir):
def getDirContent(dir=env.input_dir):
# TODO What if item in db is not in this list?
try:
return [d for d in os.listdir(dir) if d[0] != '.']
except FileNotFoundError:
# TODO Log to error file
logging.info('Error: "' + dir + '" is not a directory.')
# TODO Remove this exit(0)
# Hashes the contents of media folder to easily check for changes.
def directoryChecksum():
@@ -185,12 +189,12 @@ def XOR(list1, list2):
def filterChildItems(parent):
try:
children = getDirContent('/'.join([env.show_dir, parent]))
children = getDirContent('/'.join([env.input_dir, parent]))
if children:
strayEpisode(parent, children)
except FileNotFoundError:
# TODO Log to error file
logging.info('Error: "' + '/'.join([env.show_dir, parent]) + '" is not a valid directory.')
logging.info('Error: "' + '/'.join([env.input_dir, parent]) + '" is not a valid directory.')
def getNewItems():
newItems = XOR(getDirContent(), getShowNames())
@@ -209,7 +213,7 @@ def main():
if __name__ == '__main__':
if (os.path.exists(env.logfile)):
logging.basicConfig(filename=env.logfile, level=logging.INFO)
logging.basicConfig(filename=env.logfile, level=logging.DEBUG)
else:
print('Logfile could not be found at ' + env.logfile + '. Verifiy presence or disable logging in config.')
exit(0)
@@ -217,3 +221,4 @@ if __name__ == '__main__':
while True:
main()
sleep(30)

279
app/core.py Executable file
View File

@@ -0,0 +1,279 @@
#!/usr/bin/env python3.6
# -*- coding: utf-8 -*-
# @Author: KevinMidboe
# @Date: 2017-08-25 23:22:27
# @Last Modified by: KevinMidboe
# @Last Modified time: 2017-10-12 22:44:27
from guessit import guessit
import os, errno
import logging
import tvdb_api
from pprint import pprint
import env_variables as env
from video import VIDEO_EXTENSIONS, Episode, Movie, Video
from subtitle import SUBTITLE_EXTENSIONS, Subtitle, get_subtitle_path
from utils import sanitize
logging.basicConfig(filename=os.path.dirname(__file__) + '/' + env.logfile, level=logging.INFO)
from datetime import datetime
#: Supported archive extensions
ARCHIVE_EXTENSIONS = ('.rar',)
def scan_video(path):
"""Scan a video from a `path`.
:param str path: existing path to the video.
:return: the scanned video.
:rtype: :class:`~subliminal.video.Video`
"""
# check for non-existing path
if not os.path.exists(path):
raise ValueError('Path does not exist')
# check video extension
# if not path.endswith(VIDEO_EXTENSIONS):
# raise ValueError('%r is not a valid video extension' % os.path.splitext(path)[1])
dirpath, filename = os.path.split(path)
logging.info('Scanning video %r in %r', filename, dirpath)
# guess
parent_path = path.strip(filename)
# video = Video.fromguess(filename, parent_path, guessit(path))
video = Video(filename)
# guessit(path)
return video
def scan_subtitle(path):
if not os.path.exists(path):
raise ValueError('Path does not exist')
dirpath, filename = os.path.split(path)
logging.info('Scanning subtitle %r in %r', filename, dirpath)
# guess
parent_path = path.strip(filename)
subtitle = Subtitle.fromguess(filename, parent_path, guessit(path))
return subtitle
def scan_files(path, age=None, archives=True):
"""Scan `path` for videos and their subtitles.
See :func:`refine` to find additional information for the video.
:param str path: existing directory path to scan.
:param datetime.timedelta age: maximum age of the video or archive.
:param bool archives: scan videos in archives.
:return: the scanned videos.
:rtype: list of :class:`~subliminal.video.Video`
"""
# check for non-existing path
if not os.path.exists(path):
raise ValueError('Path does not exist')
# check for non-directory path
if not os.path.isdir(path):
raise ValueError('Path is not a directory')
name_dict = {}
# walk the path
mediafiles = []
for dirpath, dirnames, filenames in os.walk(path):
logging.debug('Walking directory %r', dirpath)
# remove badly encoded and hidden dirnames
for dirname in list(dirnames):
if dirname.startswith('.'):
logging.debug('Skipping hidden dirname %r in %r', dirname, dirpath)
dirnames.remove(dirname)
# scan for videos
for filename in filenames:
# filter on videos and archives
if not (filename.endswith(VIDEO_EXTENSIONS) or filename.endswith(SUBTITLE_EXTENSIONS) or archives and filename.endswith(ARCHIVE_EXTENSIONS)):
continue
# skip hidden files
if filename.startswith('.'):
logging.debug('Skipping hidden filename %r in %r', filename, dirpath)
continue
# reconstruct the file path
filepath = os.path.join(dirpath, filename)
# skip links
if os.path.islink(filepath):
logging.debug('Skipping link %r in %r', filename, dirpath)
continue
# skip old files
if age and datetime.utcnow() - datetime.utcfromtimestamp(os.path.getmtime(filepath)) > age:
logging.debug('Skipping old file %r in %r', filename, dirpath)
continue
# scan
if filename.endswith(VIDEO_EXTENSIONS): # video
try:
video = scan_video(filepath)
try:
name_dict[video.series] += 1
except KeyError:
name_dict[video.series] = 0
mediafiles.append(video)
except ValueError: # pragma: no cover
logging.exception('Error scanning video')
continue
elif archives and filename.endswith(ARCHIVE_EXTENSIONS): # archive
print('archive')
pass
# try:
# video = scan_archive(filepath)
# mediafiles.append(video)
# except (NotRarFile, RarCannotExec, ValueError): # pragma: no cover
# logging.exception('Error scanning archive')
# continue
elif filename.endswith(SUBTITLE_EXTENSIONS): # subtitle
try:
subtitle = scan_subtitle(filepath)
mediafiles.append(subtitle)
except ValueError:
logging.exception('Error scanning subtitle')
continue
else: # pragma: no cover
raise ValueError('Unsupported file %r' % filename)
pprint(name_dict)
return mediafiles
def organize_files(path):
hashList = {}
mediafiles = scan_files(path)
# print(mediafiles)
for file in mediafiles:
hashList.setdefault(file.__hash__(),[]).append(file)
# hashList[file.__hash__()] = file
return hashList
def save_subtitles(files, single=False, directory=None, encoding=None):
t = tvdb_api.Tvdb()
if not isinstance(files, list):
files = [files]
for file in files:
# TODO this should not be done in the loop
dirname = "%s S%sE%s" % (file.series, "%02d" % (file.season), "%02d" % (file.episode))
createParentfolder = not dirname in file.parent_path
if createParentfolder:
dirname = os.path.join(file.parent_path, dirname)
print('Created: %s' % dirname)
try:
os.makedirs(dirname)
except OSError as e:
if e.errno != errno.EEXIST:
raise
# TODO Clean this !
try:
tvdb_episode = t[file.series][file.season][file.episode]
episode_title = tvdb_episode['episodename']
except:
episode_title = ''
old = os.path.join(file.parent_path, file.name)
if file.name.endswith(SUBTITLE_EXTENSIONS):
lang = file.getLanguage()
sdh = '.sdh' if file.sdh else ''
filename = "%s S%sE%s %s%s.%s.%s" % (file.series, "%02d" % (file.season), "%02d" % (file.episode), episode_title, sdh, lang, file.container)
else:
filename = "%s S%sE%s %s.%s" % (file.series, "%02d" % (file.season), "%02d" % (file.episode), episode_title, file.container)
if createParentfolder:
newname = os.path.join(dirname, filename)
else:
newname = os.path.join(file.parent_path, filename)
print('Moved: %s ---> %s' % (old, newname))
os.rename(old, newname)
print()
# for hash in files:
# hashIndex = [files[hash]]
# for hashItems in hashIndex:
# for file in hashItems:
# print(file.series)
# saved_subtitles = []
# for subtitle in files:
# # check content
# if subtitle.name is None:
# logging.error('Skipping subtitle %r: no content', subtitle)
# continue
# # check language
# if subtitle.language in set(s.language for s in saved_subtitles):
# logging.debug('Skipping subtitle %r: language already saved', subtitle)
# continue
# # create subtitle path
# subtitle_path = get_subtitle_path(video.name, None if single else subtitle.language)
# if directory is not None:
# subtitle_path = os.path.join(directory, os.path.split(subtitle_path)[1])
# # save content as is or in the specified encoding
# logging.info('Saving %r to %r', subtitle, subtitle_path)
# if encoding is None:
# with io.open(subtitle_path, 'wb') as f:
# f.write(subtitle.content)
# else:
# with io.open(subtitle_path, 'w', encoding=encoding) as f:
# f.write(subtitle.text)
# saved_subtitles.append(subtitle)
# # check single
# if single:
# break
# return saved_subtitles
def stringTime():
return str(datetime.now().strftime("%Y-%m-%d %H:%M:%S:%f"))
def main():
# episodePath = '/Volumes/media/tv/Black Mirror/Black Mirror Season 01/'
episodePath = '/Volumes/mainframe/shows/Black Mirror/Black Mirror Season 01/'
t = tvdb_api.Tvdb()
hashList = organize_files(episodePath)
pprint(hashList)
if __name__ == '__main__':
main()

99
app/magnet.py Executable file
View File

@@ -0,0 +1,99 @@
#!/usr/bin/env python
'''
Created on Apr 19, 2012
@author: dan, Faless
GNU GENERAL PUBLIC LICENSE - Version 3
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
http://www.gnu.org/licenses/gpl-3.0.txt
'''
import shutil
import tempfile
import os.path as pt
import sys, logging
import libtorrent as lt
from time import sleep
import env_variables as env
logging.basicConfig(filename=pt.dirname(__file__) + '/' + env.logfile)
def magnet2torrent(magnet, output_name=None):
if output_name and \
not pt.isdir(output_name) and \
not pt.isdir(pt.dirname(pt.abspath(output_name))):
logging.info("Invalid output folder: " + pt.dirname(pt.abspath(output_name)))
logging.info("")
sys.exit(0)
tempdir = tempfile.mkdtemp()
ses = lt.session()
params = {
'save_path': tempdir,
'storage_mode': lt.storage_mode_t(2),
'paused': False,
'auto_managed': True,
'duplicate_is_error': True
}
handle = lt.add_magnet_uri(ses, magnet, params)
logging.info("Downloading Metadata (this may take a while)")
while (not handle.has_metadata()):
try:
sleep(1)
except KeyboardInterrupt:
logging.info("Aborting...")
ses.pause()
logging.info("Cleanup dir " + tempdir)
shutil.rmtree(tempdir)
sys.exit(0)
ses.pause()
logging.info("Done")
torinfo = handle.get_torrent_info()
torfile = lt.create_torrent(torinfo)
output = pt.abspath(torinfo.name() + ".torrent")
if output_name:
if pt.isdir(output_name):
output = pt.abspath(pt.join(
output_name, torinfo.name() + ".torrent"))
elif pt.isdir(pt.dirname(pt.abspath(output_name))):
output = pt.abspath(output_name)
logging.info("Saving torrent file here : " + output + " ...")
torcontent = lt.bencode(torfile.generate())
f = open(output, "wb")
f.write(lt.bencode(torfile.generate()))
f.close()
logging.info("Saved! Cleaning up dir: " + tempdir)
ses.remove_torrent(handle)
shutil.rmtree(tempdir)
return output
def main():
magnet = sys.argv[1]
logging.info('INPUT: {}'.format(magnet))
magnet2torrent(magnet, env.torrent_dumpsite)
if __name__ == "__main__":
main()

112
app/moveSeasoned.py Executable file
View File

@@ -0,0 +1,112 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# @Author: KevinMidboe
# @Date: 2017-04-12 23:27:51
# @Last Modified by: KevinMidboe
# @Last Modified time: 2018-05-13 19:17:17
import sys, sqlite3, json, os.path
import logging
import env_variables as env
import shutil
import delugeClient.deluge_cli as delugeCli
class episode(object):
def __init__(self, id):
self.id = id
self.getVarsFromDB()
def getVarsFromDB(self):
c = sqlite3.connect(env.db_path).cursor()
c.execute('SELECT parent, name, season, episode, video_files, subtitles, trash FROM stray_eps WHERE id = ?', (self.id,))
returnMsg = c.fetchone()
self.parent = returnMsg[0]
self.name = returnMsg[1]
self.season = returnMsg[2]
self.episode = returnMsg[3]
self.video_files = json.loads(returnMsg[4])
self.subtitles = json.loads(returnMsg[5])
self.trash = json.loads(returnMsg[6])
c.close()
self.queries = {
'parent_input': [env.input_dir, self.parent],
'season': [env.show_dir, self.name, self.name + ' Season ' + "%02d" % self.season],
'episode': [env.show_dir, self.name, self.name + ' Season ' + "%02d" % self.season, \
self.name + ' S' + "%02d" % self.season + 'E' + "%02d" % self.episode],
}
def typeDir(self, dType, create=False, mergeItem=None):
url = '/'.join(self.queries[dType])
print(url)
if create and not os.path.isdir(url):
os.makedirs(url)
fix_ownership(url)
if mergeItem:
return '/'.join([url, str(mergeItem)])
return url
def fix_ownership(path):
pass
# TODO find this from username from config
# uid = 1000
# gid = 112
# os.chown(path, uid, gid)
def moveStray(strayId):
ep = episode(strayId)
for item in ep.video_files:
try:
old_dir = ep.typeDir('parent_input', mergeItem=item[0])
new_dir = ep.typeDir('episode', mergeItem=item[1], create=True)
shutil.move(old_dir, new_dir)
except FileNotFoundError:
logging.warning(old_dir + ' does not exits, cannot be moved.')
for item in ep.subtitles:
try:
old_dir = ep.typeDir('parent_input', mergeItem=item[0])
new_dir = ep.typeDir('episode', mergeItem=item[1], create=True)
shutil.move(old_dir, new_dir)
except FileNotFoundError:
logging.warning(old_dir + ' does not exits, cannot be moved.')
for item in ep.trash:
try:
os.remove(ep.typeDir('parent_input', mergeItem=item))
except FileNotFoundError:
logging.warning(ep.typeDir('parent_input', mergeItem=item) + 'does not exist, cannot be removed.')
fix_ownership(ep.typeDir('episode'))
for root, dirs, files in os.walk(ep.typeDir('episode')):
for item in files:
fix_ownership(os.path.join(ep.typeDir('episode'), item))
# TODO because we might jump over same files, the dir might no longer
# be empty and cannot remove dir like this.
try:
os.rmdir(ep.typeDir('parent_input'))
except FileNotFoundError:
logging.warning('Cannot remove ' + ep.typeDir('parent_input') + ', file no longer exists.')
# Remove from deluge client
logging.info('Removing {} for deluge'.format(ep.parent))
deluge = delugeCli.Deluge()
response = deluge.remove(ep.parent)
logging.info('Deluge response after delete: {}'.format(response))
if __name__ == '__main__':
abspath = os.path.abspath(__file__)
dname = os.path.dirname(abspath)
if (os.path.exists(os.path.join(dname, env.logfile))):
logging.basicConfig(filename=env.logfile, level=logging.INFO)
else:
print('Logfile could not be found at ' + env.logfile + '. Verifiy presence or disable logging in config.')
moveStray(sys.argv[-1])

318
app/pirateSearch.py Executable file
View File

@@ -0,0 +1,318 @@
#!/usr/bin/env python3.6
# -*- coding: utf-8 -*-
# @Author: KevinMidboe
# @Date: 2017-10-12 11:55:03
# @Last Modified by: KevinMidboe
# @Last Modified time: 2017-10-22 18:54:18
import sys, logging, re, json
from urllib import parse, request
from urllib.error import URLError
from bs4 import BeautifulSoup
from os import path
import datetime
from pprint import pprint
from core import stringTime
import env_variables as env
logging.basicConfig(filename=path.dirname(__file__) + '/' + env.logfile, level=logging.INFO)
RELEASE_TYPES = ('bdremux', 'brremux', 'remux',
'bdrip', 'brrip', 'blu-ray', 'bluray', 'bdmv', 'bdr', 'bd5',
'web-cap', 'webcap', 'web cap',
'webrip', 'web rip', 'web-rip', 'web',
'webdl', 'web dl', 'web-dl', 'hdrip',
'dsr', 'dsrip', 'satrip', 'dthrip', 'dvbrip', 'hdtv', 'pdtv', 'tvrip', 'hdtvrip',
'dvdr', 'dvd-full', 'full-rip', 'iso',
'ts', 'hdts', 'hdts', 'telesync', 'pdvd', 'predvdrip',
'camrip', 'cam')
def sanitize(string, ignore_characters=None, replace_characters=None):
"""Sanitize a string to strip special characters.
:param str string: the string to sanitize.
:param set ignore_characters: characters to ignore.
:return: the sanitized string.
:rtype: str
"""
# only deal with strings
if string is None:
return
replace_characters = replace_characters or ''
ignore_characters = ignore_characters or set()
characters = ignore_characters
if characters:
string = re.sub(r'[%s]' % re.escape(''.join(characters)), replace_characters, string)
return string
def return_re_match(string, re_statement):
if string is None:
return
m = re.search(re_statement, string)
if 'Y-day' in m.group():
return datetime.timedelta(days=1).strftime('%m-%d %Y')
if 'Today' in m.group():
return datetime.datetime.now().strftime('%m-%d %Y')
return sanitize(m.group(), '\xa0', ' ')
# Can maybe be moved away from this class
# returns a number that is either the value of multiple_pages
# or if it exceeds total_pages, return total_pages.
def pagesToCount(multiple, total):
if (multiple > total):
return total
return multiple
# Should maybe not be able to set values without checking if they are valid?
class piratebay(object):
def __init__(self, query=None, page=0, sort=None, category=None):
# This should be moved to a config file
self.url = 'https://thepiratebay.org/search'
self.sortTypes = {
'size': 5,
'seed_count': 99
}
self.categoryTypes = {
'movies': 207,
'porn_movies': 505,
}
# - - -
# Req params
self.query = query
self.page = page
self.sort = sort
self.category = category
self.total_pages = 0
self.headers = {'User-Agent': 'Mozilla/5.0'}
# self.headers = {}
def build_URL_request(self):
url = '/'.join([self.url, parse.quote(self.query), str(self.page), str(self.sort), str(self.category)])
return request.Request(url, headers=self.headers)
def next_page(self):
# If page exceeds the max_page, return None
# Can either save the last query/url in the object or have it passed
# again on call to next_page
# Throw a error if it is not possible (overflow)
self.page += 1
raw_page = self.callPirateBaT()
return self.parse_raw_page_for_torrents(raw_page)
def set_total_pages(self, raw_page):
# body-id:searchResults-id:content-align:center
soup = BeautifulSoup(raw_page, 'html.parser')
content_searchResult = soup.body.find(id='SearchResults')
page_div = content_searchResult.find_next(attrs={"align": "center"})
last_page = 0
for page in page_div.find_all('a'):
last_page += 1
self.total_pages = last_page
def callPirateBaT(self):
req = self.build_URL_request()
raw_page = self.fetchURL(req).read()
logging.info('Finished searching piratebay for query | %s' % stringTime())
if raw_page is None:
raise ValueError('Search result returned no content. Please check log for error reason.')
if self.total_pages is 0:
self.set_total_pages(raw_page)
return raw_page
# Sets the search
def search(self, query, multiple_pages=1, page=0, sort=None, category=None):
# This should not be logged here, but in loop. Something else here maybe?
logging.info('Searching piratebay with query: %r, sort: %s and category: %s | %s' %
(query, sort, category, stringTime()))
if sort is not None and sort in self.sortTypes:
self.sort = self.sortTypes[sort]
else:
raise ValueError('Invalid sort category for piratebay search')
# Verify input? and reset total_pages
self.query = query
self.total_pages = 0
if str(page).isnumeric() and type(page) == int and page >= 0:
self.page = page
# TODO add category list
if category is not None and category in self.categoryTypes:
self.category = self.categoryTypes[category]
# TODO Pull most of this logic out bc it needs to also be done in next_page
raw_page = self.callPirateBaT()
torrents_found = self.parse_raw_page_for_torrents(raw_page)
# Fetch in parallel
n = pagesToCount(multiple_pages, self.total_pages)
while n > 1:
torrents_found.extend(self.next_page())
n -= 1
return torrents_found
def removeHeader(self, bs4_element):
if ('header' in bs4_element['class']):
return bs4_element.find_next('tr')
return bs4_element
def has_magnet(self, href):
return href and re.compile('magnet').search(href)
def parse_raw_page_for_torrents(self, content):
soup = BeautifulSoup(content, 'html.parser')
content_searchResult = soup.body.find(id='searchResult')
if content_searchResult is None:
logging.info('No torrents found for the search criteria.')
return None
listElements = content_searchResult.tr
torrentWrapper = self.removeHeader(listElements)
torrents_found = []
for torrentElement in torrentWrapper.find_all_next('td'):
if torrentElement.find_all("div", class_='detName'):
name = torrentElement.find('a', class_='detLink').get_text()
url = torrentElement.find('a', class_='detLink')['href']
magnet = torrentElement.find(href=self.has_magnet)
uploader = torrentElement.find('a', class_='detDesc')
if uploader is None:
uploader = torrentElement.find('i')
uploader = uploader.get_text()
info_text = torrentElement.find('font', class_='detDesc').get_text()
date = return_re_match(info_text, r"(\d+\-\d+(\s\d{4})?)|(Y\-day|Today)")
size = return_re_match(info_text, r"(\d+(\.\d+)?\s[a-zA-Z]+)")
# COULD NOT FIND HREF!
if (magnet is None):
continue
seed_and_leech = torrentElement.find_all_next(attrs={"align": "right"})
seed = seed_and_leech[0].get_text()
leech = seed_and_leech[1].get_text()
torrent = Torrent(name, magnet['href'], size, uploader, date, seed, leech, url)
torrents_found.append(torrent)
else:
# print(torrentElement)
continue
logging.info('Found %s torrents for given search criteria.' % len(torrents_found))
return torrents_found
def fetchURL(self, req):
try:
response = request.urlopen(req)
except URLError as e:
if hasattr(e, 'reason'):
logging.error('We failed to reach a server with request: %s' % req.full_url)
logging.error('Reason: %s' % e.reason)
elif hasattr(e, 'code'):
logging.error('The server couldn\'t fulfill the request.')
logging.error('Error code: ', e.code)
else:
return response
class Torrent(object):
def __init__(self, name, magnet=None, size=None, uploader=None, date=None,
seed_count=None, leech_count=None, url=None):
self.name = name
self.magnet = magnet
self.size = size
self.uploader = uploader
self.date = date
self.seed_count = seed_count
self.leech_count = leech_count
self.url = url
def find_release_type(self):
name = self.name.casefold()
return [r_type for r_type in RELEASE_TYPES if r_type in name]
def get_all_attr(self):
return ({'name': self.name, 'magnet': self.magnet,'uploader': self.uploader,'size': self.size,'date': self.date,'seed': self.seed_count,'leech': self.leech_count,'url': self.url})
def __repr__(self):
return '<%s [%r]>' % (self.__class__.__name__, self.name)
# This should be done front_end!
# I.E. filtering like this should be done in another script
# and should be done with the shared standard for types.
# PS: Is it the right move to use a shared standard? What
# happens if it is no longer public?
def chooseCandidate(torrent_list):
interesting_torrents = []
match_release_type = ['bdremux', 'brremux', 'remux', 'bdrip', 'brrip', 'blu-ray', 'bluray', 'bdmv', 'bdr', 'bd5']
for torrent in torrent_list:
intersecting_release_types = set(torrent.find_release_type()) & set(match_release_type)
size, _, size_id = torrent.size.partition(' ')
if intersecting_release_types and int(torrent.seed_count) > 0 and float(size) > 4 and size_id == 'GiB':
# print('{} : {} : {} {}'.format(torrent.name, torrent.size, torrent.seed_count, torrent.magnet))
interesting_torrents.append(torrent.get_all_attr())
# else:
# print('Denied match! %s : %s : %s' % (torrent.name, torrent.size, torrent.seed_count))
return interesting_torrents
def searchTorrentSite(query, site='piratebay'):
pirate = piratebay()
torrents_found = pirate.search(query, page=0, multiple_pages=3, sort='size')
candidates = {}
if (torrents_found):
candidates = chooseCandidate(torrents_found)
print(json.dumps(candidates))
# torrents_found = pirate.next_page()
# pprint(torrents_found)
# candidates = chooseCandidate(torrents_found)
# Can autocall to next_page in a looped way to get more if nothing is found
# and there is more pages to be looked at
def main():
query = sys.argv[1]
searchTorrentSite(query)
if __name__ == '__main__':
main()

57
app/seasonMover.py Executable file
View File

@@ -0,0 +1,57 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# @Author: KevinMidboe
# @Date: 2017-07-11 19:16:23
# @Last Modified by: KevinMidboe
# @Last Modified time: 2017-07-11 19:16:23
import fire, re, os
class seasonMover(object):
''' Moving multiple files to multiple folders with
identifer '''
workingDir = os.getcwd()
def create(self, name, interval):
pass
def move(self, fileSyntax, folderName):
episodeRange = self.findInterval(fileSyntax)
self.motherMover(fileSyntax, folderName, episodeRange)
def findInterval(self, item):
if (re.search(r'\((.*)\)', item) is None):
raise ValueError('Need to declare an identifier e.g. (1..3) in: \n\t' + item)
start = int(re.search('\((\d+)\.\.', item).group(1))
end = int(re.search('\.\.(\d+)\)', item).group(1))
return list(range(start, end+1))
def removeUploadSign(self, file):
match = re.search('-[a-zA-Z\[\]\-]*.[a-z]{3}', file)
if match:
uploader = match.group(0)[:-4]
return re.sub(uploader, '', file)
return file
def motherMover(self, fileSyntax, folderName, episodeRange):
# Call for sub of fileList
# TODO check if range is same as folderContent
for episode in episodeRange:
leadingZeroNumber = "%02d" % episode
fileName = re.sub(r'\((.*)\)', leadingZeroNumber, fileSyntax)
oldPath = os.path.join(self.workingDir,fileName)
newFolder = os.path.join(self.workingDir, folderName + leadingZeroNumber)
newPath = os.path.join(newFolder, self.removeUploadSign(fileName))
os.makedirs(newFolder)
os.rename(oldPath, newPath)
# print(newFolder)
# print(oldPath + ' --> ' + newPath)
if __name__ == '__main__':
fire.Fire(seasonMover)

111
app/subtitle.py Normal file
View File

@@ -0,0 +1,111 @@
# -*- coding: utf-8 -*-
import codecs
import logging
import os
import chardet
import hashlib
from video import Episode, Movie
from utils import sanitize
from langdetect import detect
logger = logging.getLogger(__name__)
#: Subtitle extensions
SUBTITLE_EXTENSIONS = ('.srt', '.sub')
class Subtitle(object):
"""Base class for subtitle.
:param language: language of the subtitle.
:type language: :class:`~babelfish.language.Language`
:param bool hearing_impaired: whether or not the subtitle is hearing impaired.
:param page_link: URL of the web page from which the subtitle can be downloaded.
:type page_link: str
:param encoding: Text encoding of the subtitle.
:type encoding: str
"""
#: Name of the provider that returns that class of subtitle
provider_name = ''
def __init__(self, name, parent_path, series, season, episode, language=None, hash=None, container=None, format=None, sdh=False):
#: Language of the subtitle
self.name = name
self.parent_path = parent_path
self.series = series
self.season = season
self.episode = episode
self.language=language
self.hash = hash
self.container = container
self.format = format
self.sdh = sdh
@classmethod
def fromguess(cls, name, parent_path, guess):
if not (guess['type'] == 'movie' or guess['type'] == 'episode'):
raise ValueError('The guess must be an episode guess')
if 'title' not in guess:
raise ValueError('Insufficient data to process the guess')
sdh = 'sdh' in name.lower()
if guess['type'] is 'episode':
return cls(name, parent_path, guess.get('title', 1), guess.get('season'), guess['episode'],
container=guess.get('container'), format=guess.get('format'), sdh=sdh)
elif guess['type'] is 'movie':
return cls(name, parent_path, guess.get('title', 1), container=guess.get('container'),
format=guess.get('format'), sdh=sdh)
def getLanguage(self):
f = open(os.path.join(self.parent_path, self.name), 'r', encoding='ISO-8859-15')
language = detect(f.read())
f.close()
return language
def __hash__(self):
return hashlib.md5("b'{}'".format(str(self.series) + str(self.season) + str(self.episode)).encode()).hexdigest()
def __repr__(self):
return '<%s %s [%sx%s]>' % (self.__class__.__name__, self.series, self.season, str(self.episode))
def get_subtitle_path(subtitles_path, language=None, extension='.srt'):
"""Get the subtitle path using the `subtitles_path` and `language`.
:param str subtitles_path: path to the subtitle.
:param language: language of the subtitle to put in the path.
:type language: :class:`~babelfish.language.Language`
:param str extension: extension of the subtitle.
:return: path of the subtitle.
:rtype: str
"""
subtitle_root = os.path.splitext(subtitles_path)[0]
if language:
subtitle_root += '.' + str(language)
return subtitle_root + extension

38
app/utils.py Normal file
View File

@@ -0,0 +1,38 @@
# -*- coding: utf-8 -*-
from datetime import datetime
import hashlib
import os
import re
import struct
def sanitize(string, ignore_characters=None):
"""Sanitize a string to strip special characters.
:param str string: the string to sanitize.
:param set ignore_characters: characters to ignore.
:return: the sanitized string.
:rtype: str
"""
# only deal with strings
if string is None:
return
ignore_characters = ignore_characters or set()
# replace some characters with one space
# characters = {'-', ':', '(', ')', '.'} - ignore_characters
# if characters:
# string = re.sub(r'[%s]' % re.escape(''.join(characters)), ' ', string)
# remove some characters
characters = {'\''} - ignore_characters
if characters:
string = re.sub(r'[%s]' % re.escape(''.join(characters)), '', string)
# replace multiple spaces with one
string = re.sub(r'\s+', ' ', string)
# strip and lower case
return string.strip().lower()

233
app/video.py Normal file
View File

@@ -0,0 +1,233 @@
#!/usr/bin/env python3.6
# -*- coding: utf-8 -*-
# @Author: KevinMidboe
# @Date: 2017-08-26 08:23:18
# @Last Modified by: KevinMidboe
# @Last Modified time: 2017-09-29 13:56:21
from guessit import guessit
import os
import hashlib, tvdb_api
#: Video extensions
VIDEO_EXTENSIONS = ('.3g2', '.3gp', '.3gp2', '.3gpp', '.60d', '.ajp', '.asf', '.asx', '.avchd', '.avi', '.bik',
'.bix', '.box', '.cam', '.dat', '.divx', '.dmf', '.dv', '.dvr-ms', '.evo', '.flc', '.fli',
'.flic', '.flv', '.flx', '.gvi', '.gvp', '.h264', '.m1v', '.m2p', '.m2ts', '.m2v', '.m4e',
'.m4v', '.mjp', '.mjpeg', '.mjpg', '.mkv', '.moov', '.mov', '.movhd', '.movie', '.movx', '.mp4',
'.mpe', '.mpeg', '.mpg', '.mpv', '.mpv2', '.mxf', '.nsv', '.nut', '.ogg', '.ogm' '.ogv', '.omf',
'.ps', '.qt', '.ram', '.rm', '.rmvb', '.swf', '.ts', '.vfw', '.vid', '.video', '.viv', '.vivo',
'.vob', '.vro', '.wm', '.wmv', '.wmx', '.wrap', '.wvx', '.wx', '.x264', '.xvid')
class Video(object):
"""Base class for videos.
Represent a video, existing or not.
:param str name: name or path of the video.
:param str format: format of the video (HDTV, WEB-DL, BluRay, ...).
:param str release_group: release group of the video.
:param str resolution: resolution of the video stream (480p, 720p, 1080p or 1080i).
:param str video_codec: codec of the video stream.
:param str audio_codec: codec of the main audio stream.
:param str imdb_id: IMDb id of the video.
:param dict hashes: hashes of the video file by provider names.
:param int size: size of the video file in bytes.
:param set subtitle_languages: existing subtitle languages.
"""
def __init__(self, name, format=None, release_group=None, resolution=None, video_codec=None, audio_codec=None,
imdb_id=None, hashes=None, size=None, subtitle_languages=None):
#: Name or path of the video
self.name = name
#: Format of the video (HDTV, WEB-DL, BluRay, ...)
self.format = format
#: Release group of the video
self.release_group = release_group
#: Resolution of the video stream (480p, 720p, 1080p or 1080i)
self.resolution = resolution
#: Codec of the video stream
self.video_codec = video_codec
#: Codec of the main audio stream
self.audio_codec = audio_codec
#: IMDb id of the video
self.imdb_id = imdb_id
#: Hashes of the video file by provider names
self.hashes = hashes or {}
#: Size of the video file in bytes
self.size = size
#: Existing subtitle languages
self.subtitle_languages = subtitle_languages or set()
@property
def exists(self):
"""Test whether the video exists"""
return os.path.exists(self.name)
@property
def age(self):
"""Age of the video"""
if self.exists:
return datetime.utcnow() - datetime.utcfromtimestamp(os.path.getmtime(self.name))
return timedelta()
@classmethod
def fromguess(cls, name, parent_path, guess):
"""Create an :class:`Episode` or a :class:`Movie` with the given `name` based on the `guess`.
:param str name: name of the video.
:param dict guess: guessed data.
:raise: :class:`ValueError` if the `type` of the `guess` is invalid
"""
if guess['type'] == 'episode':
return Episode.fromguess(name, parent_path, guess)
if guess['type'] == 'movie':
return Movie.fromguess(name, guess)
raise ValueError('The guess must be an episode or a movie guess')
@classmethod
def fromname(cls, name):
"""Shortcut for :meth:`fromguess` with a `guess` guessed from the `name`.
:param str name: name of the video.
"""
return cls.fromguess(name, guessit(name))
def __repr__(self):
return '<%s [%r]>' % (self.__class__.__name__, self.name)
def __hash__(self):
return hash(self.name)
class Episode():
"""Episode :class:`Video`.
:param str series: series of the episode.
:param int season: season number of the episode.
:param int episode: episode number of the episode.
:param str title: title of the episode.
:param int year: year of the series.
:param bool original_series: whether the series is the first with this name.
:param int tvdb_id: TVDB id of the episode.
:param \*\*kwargs: additional parameters for the :class:`Video` constructor.
"""
def __init__(self, name, parent_path, series, season, episode, year=None, original_series=True, tvdb_id=None,
series_tvdb_id=None, series_imdb_id=None, release_group=None, video_codec=None, container=None,
format=None, screen_size=None, **kwargs):
super(Episode, self).__init__()
self.name = name
self.parent_path = parent_path
#: Series of the episode
self.series = series
#: Season number of the episode
self.season = season
#: Episode number of the episode
self.episode = episode
#: Year of series
self.year = year
#: The series is the first with this name
self.original_series = original_series
#: TVDB id of the episode
self.tvdb_id = tvdb_id
#: TVDB id of the series
self.series_tvdb_id = series_tvdb_id
#: IMDb id of the series
self.series_imdb_id = series_imdb_id
# The release group of the episode
self.release_group = release_group
# The video vodec of the series
self.video_codec = video_codec
# The Video container of the episode
self.container = container
# The Video format of the episode
self.format = format
# The Video screen_size of the episode
self.screen_size = screen_size
@classmethod
def fromguess(cls, name, parent_path, guess):
if guess['type'] != 'episode':
raise ValueError('The guess must be an episode guess')
if 'title' not in guess or 'episode' not in guess:
raise ValueError('Insufficient data to process the guess')
return cls(name, parent_path, guess['title'], guess.get('season', 1), guess['episode'],
year=guess.get('year'), original_series='year' not in guess, release_group=guess.get('release_group'),
video_codec=guess.get('video_codec'), audio_codec=guess.get('audio_codec'), container=guess.get('container'),
format=guess.get('format'), screen_size=guess.get('screen_size'))
@classmethod
def fromname(cls, name):
return cls.fromguess(name, guessit(name, {'type': 'episode'}))
def __hash__(self):
return hashlib.md5("b'{}'".format(str(self.series) + str(self.season) + str(self.episode)).encode()).hexdigest()
# THE EP NUMBER IS CONVERTED TO STRING AS A QUICK FIX FOR MULTIPLE NUMBERS IN ONE
def __repr__(self):
if self.year is None:
return '<%s [%r, %sx%s]>' % (self.__class__.__name__, self.series, self.season, str(self.episode))
return '<%s [%r, %d, %sx%s]>' % (self.__class__.__name__, self.series, self.year, self.season, str(self.episode))
class Movie():
"""Movie :class:`Video`.
:param str title: title of the movie.
:param int year: year of the movie.
:param \*\*kwargs: additional parameters for the :class:`Video` constructor.
"""
def __init__(self, name, title, year=None, format=None, **kwargs):
super(Movie, self).__init__()
#: Title of the movie
self.title = title
#: Year of the movie
self.year = year
self.format = format
@classmethod
def fromguess(cls, name, guess):
if guess['type'] != 'movie':
raise ValueError('The guess must be a movie guess')
if 'title' not in guess:
raise ValueError('Insufficient data to process the guess')
return cls(name, guess['title'], format=guess.get('format'), release_group=guess.get('release_group'),
resolution=guess.get('screen_size'), video_codec=guess.get('video_codec'),
audio_codec=guess.get('audio_codec'), year=guess.get('year'))
@classmethod
def fromname(cls, name):
return cls.fromguess(name, guessit(name, {'type': 'movie'}))
def __repr__(self):
if self.year is None:
return '<%s [%r]>' % (self.__class__.__name__, self.title)
return '<%s [%r, %d]>' % (self.__class__.__name__, self.title, self.year)

58
client/.gitignore vendored Normal file
View File

@@ -0,0 +1,58 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Typescript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env

Binary file not shown.

25
client/app/Root.jsx Normal file
View File

@@ -0,0 +1,25 @@
import React, { Component } from 'react';
import { HashRouter as Router, Route, Switch, IndexRoute } from 'react-router-dom';
import SearchRequest from './components/SearchRequest.jsx';
import AdminComponent from './components/admin/Admin.jsx';
class Root extends Component {
// We need to provide a list of routes
// for our app, and in this case we are
// doing so from a Root component
render() {
return (
<Router>
<Switch>
<Route exact path='/' component={SearchRequest} />
<Route path='/admin/:request' component={AdminComponent} />
<Route path='/admin' component={AdminComponent} />
</Switch>
</Router>
);
}
}
export default Root;

41
client/app/app.scss Normal file
View File

@@ -0,0 +1,41 @@
@font-face {
font-family: "din";
src: url('/app/DIN-Regular-webfont.woff')
}
html {
font-family: 'din', 'Open Sans', sans-serif;
display: inline-block;
color:red;
}
#requestMovieList {
display: flex;
flex-wrap: wrap;
justify-content: center;
}
.movie_wrapper {
color:red;
display: flex;
align-content: center;
width: 30%;
background-color: #ffffff;
height: 231px;
margin: 20px;
-webkit-box-shadow: 0px 0px 5px 1px rgba(0,0,0,0.15);
-moz-box-shadow: 0px 0px 5px 1px rgba(0,0,0,0.15);
box-shadow: 0px 0px 5px 1px rgba(0,0,0,0.15);
}
.movie_content {
margin-left: 15px;
}
.movie_header {
font-size: 1.6em;
}

View File

@@ -1,23 +0,0 @@
/*
./app/components/App.jsx
<FetchData url={"https://apollo.kevinmidboe.com/api/v1/plex/playing"} />
*/
import React from 'react';
import FetchData from './FetchData.js';
import ListStrays from './ListStrays.jsx'
export default class App extends React.Component {
render() {
return (
<div>
<div style={{textAlign: 'center'}}>
<h1>Welcome to Seasoned</h1>
</div>
<ListStrays />
<FetchData />
</div>
);
}
}

View File

@@ -0,0 +1,26 @@
import React from 'react';
export function getCookie(cname) {
var name = cname + "=";
var decodedCookie = decodeURIComponent(document.cookie);
var ca = decodedCookie.split(';');
for(var i = 0; i <ca.length; i++) {
var c = ca[i];
while (c.charAt(0) == ' ') {
c = c.substring(1);
}
if (c.indexOf(name) == 0) {
return c.substring(name.length, c.length);
}
}
return false;
}
export function setCookie(cname, cvalue, exdays) {
var d = new Date();
d.setTime(d.getTime() + (exdays*24*60*60*1000));
var expires = "expires="+ d.toUTCString();
document.cookie = cname + "=" + cvalue + ";" + expires + ";path=/";
}

View File

@@ -4,7 +4,7 @@ class FetchData extends React.Component {
constructor(props){
super(props)
this.state = {
imgUrls: [],
playing: [],
hei: '1',
intervalId: null,
url: ''
@@ -16,11 +16,9 @@ class FetchData extends React.Component {
fetch("https://apollo.kevinmidboe.com/api/v1/plex/playing").then(
function(response){
response.json().then(function(data){
console.log(data.size);
that.setState({
imgUrls: that.state.imgUrls.concat(data.video)
playing: that.state.playing.concat(data.video)
})
console.log(data.video.title);
})
}
)
@@ -32,23 +30,30 @@ class FetchData extends React.Component {
}
getPlaying() {
console.log('Should not reach')
// Need callback to work
// Should try to clear out old requests to limit mem use
if (this.state.playing.length != 0) {
return this.state.playing.map((playingObj) => {
if (playingObj.type === 'episode') {
console.log('episode')
return ([
<span>{playingObj.title}</span>,
<span>{playingObj.season}</span>,
<span>{playingObj.episode}</span>
])
} else if (playingObj.type === 'movie') {
console.log('movie')
return ([
<span>{playingObj.title}</span>
])
}
})
} else {
return (<span>Nothing playing</span>)
}
}
render(){
return(
<div className="FetchData">
{this.state.imgUrls.map((imgObj) => {
return ([
<span>{imgObj.title}</span>,
<span>{imgObj.season}</span>,
<span>{imgObj.episode}</span>,
]);
})}
</div>
<div className="FetchData">{this.getPlaying()}</div>
);
}

View File

@@ -0,0 +1,266 @@
import React from 'react';
import requestElement from './styles/requestElementStyle.jsx'
import { getCookie } from './Cookie.jsx';
class DropdownList extends React.Component {
constructor(props) {
super(props);
this.state = {
filter: ['all', 'requested', 'downloading', 'downloaded'],
sort: ['requested_date', 'name', 'status', 'requested_by', 'ip', 'user_agent'],
status: ['requested', 'downloading', 'downloaded'],
}
}
render() {
const {elementType, elementId, elementStatus, elementCallback, props} = this.props;
console.log(elementCallback('downloaded'))
switch (elementType) {
case 'status':
return (
<div>HERE</div>
)
}
return (
<div {...props}>
</div>
);
}
}
class RequestElement extends React.Component {
constructor(props) {
super(props);
this.state = {
dropDownState: undefined,
}
}
filterRequestList(requestList, filter) {
if (filter === 'all')
return requestList
if (filter === 'movie' || filter === 'show')
return requestList.filter(item => item.type === filter)
return requestList.filter(item => item.status === filter)
}
sortRequestList(requestList, sort_type, reversed) {
requestList.sort(function(a,b) {
if(a[sort_type] < b[sort_type]) return -1;
if(a[sort_type] > b[sort_type]) return 1;
return 0;
});
if (reversed)
requestList.reverse();
}
userAgent(agent) {
if (agent) {
try {
return agent.split(" ")[1].replace(/[\(\;]/g, '');
}
catch(e) {
return agent;
}
}
return '';
}
updateDropDownState(status) {
if (status !== this.dropDownState) {
this.dropDownState = status;
}
}
ItemsStatusDropdown(id, type, status) {
return (
<div>
<select id="lang"
defaultValue={status}
onChange={event => this.updateDropDownState(event.target.value)}
>
<option value='requested'>Requested</option>
<option value='downloading'>Downloading</option>
<option value='downloaded'>Downloaded</option>
</select>
<button onClick={() => { this.updateRequestedItem(id, type)}}>Update Status</button>
</div>
)
}
updateRequestedItem(id, type) {
console.log(id, type, this.dropDownState);
Promise.resolve()
fetch('https://apollo.kevinmidboe.com/api/v1/plex/request/' + id, {
method: 'PUT',
headers: {
'Content-type': 'application/json',
'authorization': getCookie('token')
},
body: JSON.stringify({
type: type,
status: this.dropDownState,
})
})
.then(response => {
if (response.status !== 200) {
console.log('error');
}
response.json()
.then(data => {
if (data.success === true) {
console.log('UPDATED :', id, ' with ', this.dropDownState)
}
})
})
.catch(error => {
new Error(error);
})
}
createHTMLElement(data, index) {
var posterPath = 'https://image.tmdb.org/t/p/w300' + data.image_path;
return (
<div style={requestElement.wrappingDiv} key={index}>
<img style={requestElement.requestPoster} src={posterPath}></img>
<div style={requestElement.infoDiv}>
<span><b>Name</b>: {data.name} </span><br></br>
<span><b>Year</b>: {data.year}</span><br></br>
<span><b>Type</b>: {data.type}</span><br></br>
<span><b>Status</b>: {data.status}</span><br></br>
<span><b>Address</b>: {data.ip}</span><br></br>
<span><b>Requested Data:</b> {data.requested_date}</span><br></br>
<span><b>Requested By:</b> {data.requested_by}</span><br></br>
<span><b>Agent</b>: { this.userAgent(data.user_agent) }</span><br></br>
</div>
{ this.ItemsStatusDropdown(data.id, data.type, data.status) }
</div>
)
}
render() {
const {requestedElementsList, requestedElementsFilter, requestedElementsSort, props} = this.props;
var filteredRequestedList = this.filterRequestList(requestedElementsList, requestedElementsFilter)
this.sortRequestList(filteredRequestedList, requestedElementsSort.value, requestedElementsSort.reversed)
return (
<div {...props} style={requestElement.bodyDiv}>
{filteredRequestedList.map((requestItem, index) => this.createHTMLElement(requestItem, index))}
</div>
);
}
}
class FetchRequested extends React.Component {
constructor(props){
super(props)
this.state = {
requested_objects: [],
filter: 'all',
sort: {
value: 'requested_date',
reversed: false
},
}
}
componentDidMount(){
Promise.resolve()
fetch('https://apollo.kevinmidboe.com/api/v1/plex/requests/all', {
method: 'GET',
headers: {
'Content-type': 'application/json',
'authorization': getCookie('token')
}
})
.then(response => {
if (response.status !== 200) {
console.log('error');
}
response.json()
.then(data => {
if (data.success === true) {
this.setState({
requested_objects: data.requestedItems
})
}
})
})
.catch(error => {
new Error(error);
})
}
changeFilter(filter) {
this.setState({
filter: filter
})
}
updateSort(sort=null, reverse=false) {
if (sort) {
this.setState({
sort: { value: sort, reversed: reverse }
})
}
else {
this.setState({
sort: { value: this.state.sort.value, reversed: reverse }
})
}
}
render(){
return(
<div>
<select id="lang" onChange={event => this.changeFilter(event.target.value)} value={this.state.value}>
<option value="all">All</option>
<option value="requested">Requested</option>
<option value="downloading">Downloading</option>
<option value="downloaded">Downloaded</option>
<option value='movie'>Movies</option>
<option value='show'>Shows</option>
</select>
<select id="lang" onChange={event => this.updateSort(event.target.value)} value={this.state.value}>
<option value='requested_date'>Date</option>
<option value='name'>Title</option>
<option value='status'>Status</option>
<option value='requested_by'>Requested By</option>
<option value='ip'>Address</option>
<option value='user_agent'>Agent</option>
</select>
<button onClick={() => {this.updateSort(null, !this.state.sort.reversed)}}>Reverse</button>
<RequestElement
requestedElementsList={this.state.requested_objects}
requestedElementsFilter={this.state.filter}
requestedElementsSort={this.state.sort}
/>
</div>
)
}
}
export default FetchRequested;

View File

@@ -0,0 +1,11 @@
import React from 'react'
import { Link } from 'react-router-dom'
// The Header creates links that can be used to navigate
// between routes.
const Header = () => (
<header>
</header>
)
export default Header

View File

@@ -29,7 +29,6 @@ class ListStrays extends React.Component {
{this.state.strays.map((strayObj) => {
if (strayObj.verified == 0) {
var url = "https://kevinmidboe.com/seasoned/verified.html?id=" + strayObj.id;
console.log(url);
return ([
<span key={strayObj.id}>{strayObj.name}</span>,
<a href={url}>{strayObj.id}</a>

View File

@@ -0,0 +1,10 @@
// components/NotFound.js
import React from 'react';
const NotFound = () =>
<div>
<h3>404 page not found</h3>
<p>We are sorry but the page you are looking for does not exist.</p>
</div>
export default NotFound;

View File

@@ -0,0 +1,126 @@
import React from 'react';
import Notifications, {notify} from 'react-notify-toast';
// StyleComponents
import searchObjectCSS from './styles/searchObject.jsx';
import buttonsCSS from './styles/buttons.jsx';
import InfoButton from './buttons/InfoButton.jsx';
var MediaQuery = require('react-responsive');
import { fetchJSON } from './http.jsx';
import Interactive from 'react-interactive';
class SearchObject {
constructor(object) {
this.id = object.id;
this.title = object.title;
this.year = object.year;
this.type = object.type;
this.rating = object.rating;
this.poster = object.poster_path;
this.background = object.background_path;
this.matchedInPlex = object.matchedInPlex;
this.summary = object.summary;
}
requestExisting(movie) {
console.log('Exists', movie);
}
requestMovie() {
fetchJSON('https://apollo.kevinmidboe.com/api/v1/plex/request/' + this.id + '?type='+this.type, 'POST')
.then((response) => {
console.log(response);
notify.show(this.title + ' requested!', 'success', 3000);
})
.catch((e) => {
console.error('Request movie fetch went wrong: '+ e);
})
}
getElement(index) {
const element_key = index + this.id;
if (this.poster == null || this.poster == undefined) {
var posterPath = 'https://openclipart.org/image/2400px/svg_to_png/211479/Simple-Image-Not-Found-Icon.png'
} else {
var posterPath = 'https://image.tmdb.org/t/p/w185' + this.poster;
}
var backgroundPath = 'https://image.tmdb.org/t/p/w640_and_h360_bestv2/' + this.background;
var foundInPlex;
if (this.matchedInPlex) {
foundInPlex = <Interactive
as='button'
onClick={() => {this.requestExisting(this)}}
style={buttonsCSS.submit}
focus={buttonsCSS.submit_hover}
hover={buttonsCSS.submit_hover}>
<span>Request Anyway</span>
</Interactive>;
} else {
foundInPlex = <Interactive
as='button'
onClick={() => {this.requestMovie()}}
style={buttonsCSS.submit}
focus={buttonsCSS.submit_hover}
hover={buttonsCSS.submit_hover}>
<span>&#x0002B; Request</span>
</Interactive>;
}
// TODO go away from using mediaQuery, and create custom resizer
return (
<div key={element_key}>
<Notifications />
<div style={searchObjectCSS.container} key={this.id}>
<MediaQuery minWidth={600}>
<div style={searchObjectCSS.posterContainer}>
<img style={searchObjectCSS.posterImage} id='poster' src={posterPath}></img>
</div>
<span style={searchObjectCSS.title_large}>{this.title}</span>
<br></br>
<span style={searchObjectCSS.stats_large}>
Released: { this.year } | Rating: {this.rating} | Type: {this.type}
</span>
<br></br>
<span style={searchObjectCSS.summary}>{this.summary}</span>
<br></br>
</MediaQuery>
<MediaQuery maxWidth={600}>
<img src={ backgroundPath } style={searchObjectCSS.backgroundImage}></img>
<span style={searchObjectCSS.title_small}>{this.title}</span>
<br></br>
<span style={searchObjectCSS.stats_small}>Released: {this.year} | Rating: {this.rating}</span>
</MediaQuery>
<div style={searchObjectCSS.buttons}>
{foundInPlex}
<InfoButton id={this.id} type={this.type} />
</div>
</div>
<MediaQuery maxWidth={600}>
<br />
</MediaQuery>
<div style={searchObjectCSS.dividerRow}>
<div style={searchObjectCSS.itemDivider}></div>
</div>
</div>
)
}
}
export default SearchObject;

View File

@@ -0,0 +1,464 @@
import React from 'react';
import URI from 'urijs';
import InfiniteScroll from 'react-infinite-scroller';
// StyleComponents
import searchRequestCSS from './styles/searchRequestStyle.jsx';
import SearchObject from './SearchObject.jsx';
import Loading from './images/loading.jsx'
import { fetchJSON } from './http.jsx';
import { getCookie } from './Cookie.jsx';
var MediaQuery = require('react-responsive');
// TODO add option for searching multi, movies or tv shows
class SearchRequest extends React.Component {
constructor(props){
super(props)
// Constructor with states holding the search query and the element of reponse.
this.state = {
lastApiCallURI: '',
searchQuery: '',
responseMovieList: null,
movieFilter: false,
showFilter: false,
discoverType: '',
page: 1,
resultHeader: '',
loadResults: false,
scrollHasMore: true,
loading: false,
}
this.allowedListTypes = ['discover', 'popular', 'nowplaying', 'upcoming']
this.baseUrl = 'https://apollo.kevinmidboe.com/api/v1/tmdb/list';
// this.baseUrl = 'http://localhost:31459/api/v1/tmdb/list';
this.searchUrl = 'https://apollo.kevinmidboe.com/api/v1/plex/request';
// this.searchUrl = 'http://localhost:31459/api/v1/plex/request';
}
componentWillMount(){
var that = this;
// this.setState({responseMovieList: null})
this.resetPageNumber();
this.state.loadResults = true;
this.fetchTmdbList(this.allowedListTypes[Math.floor(Math.random()*this.allowedListTypes.length)]);
}
// Handles all errors of the response of a fetch call
handleErrors(response) {
if (!response.ok)
throw Error(response.status);
return response;
}
handleQueryError(response) {
if (!response.ok) {
if (response.status === 404) {
this.setState({
responseMovieList: <h1>Nothing found for search query: { this.findQueryInURI(uri) }</h1>
})
}
console.log('handleQueryError: ', error);
}
return response;
}
// Unpacks the query value of a uri
findQueryValueInURI(uri) {
let uriSearchValues = uri.query(true);
let queryValue = uriSearchValues['query']
return queryValue;
}
// Unpacks the page value of a uri
findPageValueInURI(uri) {
let uriSearchValues = uri.query(true);
let queryValue = uriSearchValues['page']
return queryValue;
}
resetPageNumber() {
this.state.page = 1;
}
setLoading(value) {
this.setState({
loading: value
});
}
// Test this by calling missing endpoint or 404 query and see what code
// and filter the error message based on the code.
// Calls a uri and returns the response as json
callURI(uri, method, data={}) {
return fetch(uri, {
method: method,
headers: new Headers({
'Content-Type': 'application/json',
'authorization': getCookie('token'),
'loggedinuser': getCookie('loggedInUser'),
})
})
.then(response => { return response })
.catch((error) => {
throw Error(error);
});
}
// Saves the input string as a h1 element in responseMovieList state
fillResponseMovieListWithError(msg) {
this.setState({
responseMovieList: <h1>{ msg }</h1>
})
}
// Here we first call api for a search with the input uri, handle any errors
// and fill the reponseData from api into the state of reponseMovieList as movieObjects
callSearchFillMovieList(uri) {
Promise.resolve()
.then(() => this.callURI(uri, 'GET'))
.then(response => {
// If we get a error code for the request
if (!response.ok) {
if (response.status === 404) {
if (this.findPageValueInURI(new URI(response.url)) > 1) {
this.state.scrollHasMore = false;
console.log(this.state.scrollHasMore)
return null
let returnMessage = 'this is the return mesasge than will never be delivered'
let theSecondReturnMsg = 'this is the second return messag ethat will NEVE be delivered'
}
else {
let errorMsg = 'Nothing found for the search query: ' + this.findQueryValueInURI(uri);
this.fillResponseMovieListWithError(errorMsg)
}
}
else {
let errorMsg = 'Error fetching query from server ' + this.response.status;
this.fillResponseMovieListWithError(errorMsg)
}
}
// Convert to json and update the state of responseMovieList with the results of the api call
// mapped as a SearchObject.
response.json()
.then(responseData => {
if (this.state.page === 1) {
this.setState({
responseMovieList: responseData.results.map((searchResultItem, index) => this.createMovieObjects(searchResultItem, index)),
lastApiCallURI: uri // Save the value of the last sucessfull api call
})
} else {
let responseMovieObjects = responseData.results.map((searchResultItem, index) => this.createMovieObjects(searchResultItem, index));
let growingReponseMovieObjectList = this.state.responseMovieList.concat(responseMovieObjects);
this.setState({
responseMovieList: growingReponseMovieObjectList,
lastApiCallURI: uri // Save the value of the last sucessfull api call
})
}
})
.catch((error) => {
console.log('CallSearchFillMovieList: ', error)
})
})
.catch((error) => {
console.log('Something went wrong when fetching query.', error)
})
}
callListFillMovieList(uri) {
// Write loading animation
Promise.resolve()
.then(() => this.callURI(uri, 'GET', undefined))
.then(response => {
// If we get a error code for the request
if (!response.ok) {
if (response.status === 404) {
let errorMsg = 'List not found';
this.fillResponseMovieListWithError(errorMsg)
}
else {
let errorMsg = 'Error fetching list from server ' + this.response.status;
this.fillResponseMovieListWithError(errorMsg)
}
}
// Convert to json and update the state of responseMovieList with the results of the api call
// mapped as a SearchObject.
response.json()
.then(responseData => {
if (this.state.page === 1) {
this.setState({
responseMovieList: responseData.results.map((searchResultItem, index) => this.createMovieObjects(searchResultItem, index)),
lastApiCallURI: uri // Save the value of the last sucessfull api call
})
} else {
let responseMovieObjects = responseData.results.map((searchResultItem, index) => this.createMovieObjects(searchResultItem, index));
let growingReponseMovieObjectList = this.state.responseMovieList.concat(responseMovieObjects);
this.setState({
responseMovieList: growingReponseMovieObjectList,
lastApiCallURI: uri // Save the value of the last sucessfull api call
})
}
})
})
.catch((error) => {
console.log('Something went wrong when fetching query.', error)
})
}
searchSeasonedRequest() {
this.state.resultHeader = 'Search result for: ' + this.state.searchQuery;
// Build uri with the url for searching requests
var uri = new URI(this.searchUrl);
// Add input of search query and page count to the uri payload
uri = uri.search({ 'query': this.state.searchQuery, 'page': this.state.page });
if (this.state.showFilter)
uri = uri.addSearch('type', 'show');
else
if (this.state.movieFilter)
uri = uri.addSearch('type', 'movie')
// Send uri to call and fill the response list with movie/show objects
this.callSearchFillMovieList(uri);
}
fetchTmdbList(tmdbListType) {
console.log(tmdbListType)
// Check if it is a whitelisted list, this should be replaced with checking if the return call is 500
if (this.allowedListTypes.indexOf(tmdbListType) === -1)
throw Error('Invalid discover type: ' + tmdbListType);
this.state.responseMovieList = []
// Captialize the first letter of and save the discoverQueryType to resultHeader state.
this.state.resultHeader = tmdbListType.toLowerCase().replace(/\b[a-z]/g, function(letter) {
return letter.toUpperCase();
});
// Build uri with the url for searching requests
var uri = new URI(this.baseUrl);
uri.segment(tmdbListType);
// Add input of search query and page count to the uri payload
uri = uri.search({ 'page': this.state.page });
if (this.state.showFilter)
uri = uri.addSearch('type', 'show');
// Send uri to call and fill the response list with movie/show objects
this.callListFillMovieList(uri);
}
// Updates the internal state of the query search field.
updateQueryState(event){
this.setState({
searchQuery: event.target.value
});
}
// For checking if the enter key was pressed in the search field.
_handleQueryKeyPress(e) {
if (e.key === 'Enter') {
// this.fetchQuery();
// Reset page number for a new search
this.resetPageNumber();
this.searchSeasonedRequest();
}
}
// When called passes the variable to SearchObject and calls it's interal function for
// generating the wanted HTML
createMovieObjects(item, index) {
let movie = new SearchObject(item);
return movie.getElement(index);
}
toggleFilter(filterType) {
if (filterType == 'movies') {
this.setState({
movieFilter: !this.state.movieFilter
})
console.log(this.state.movieFilter);
}
else if (filterType == 'shows') {
this.setState({
showFilter: !this.state.showFilter
})
console.log(this.state.showFilter);
}
}
pageBackwards() {
if (this.state.page > 1) {
let pageNumber = this.state.page - 1;
let uri = this.state.lastApiCallURI;
// Augment the page number of the uri with a callback
uri.search(function(data) {
data.page = pageNumber;
});
// Call the api with the new uri
this.callSearchFillMovieList(uri);
// Update state of our page number after the call is done
this.state.page = pageNumber;
}
}
// TODO need to get total page number and save in a state to not overflow
pageForwards() {
// Wrap this in the check
let pageNumber = this.state.page + 1;
let uri = this.state.lastApiCallURI;
// Augment the page number of the uri with a callback
uri.search(function(data) {
data.page = pageNumber;
});
// Call the api with the new uri
this.callSearchFillMovieList(uri);
// Update state of our page number after the call is done
this.state.page = pageNumber;
}
movieToggle() {
if (this.state.movieFilter)
return <span style={searchRequestCSS.searchFilterActive}
className="search_category hvrUnderlineFromCenter"
onClick={() => {this.toggleFilter('movies')}}
id="category_active">Movies</span>
else
return <span style={searchRequestCSS.searchFilterNotActive}
className="search_category hvrUnderlineFromCenter"
onClick={() => {this.toggleFilter('movies')}}
id="category_active">Movies</span>
}
showToggle() {
if (this.state.showFilter)
return <span style={searchRequestCSS.searchFilterActive}
className="search_category hvrUnderlineFromCenter"
onClick={() => {this.toggleFilter('shows')}}
id="category_active">TV Shows</span>
else
return <span style={searchRequestCSS.searchFilterNotActive}
className="search_category hvrUnderlineFromCenter"
onClick={() => {this.toggleFilter('shows')}}
id="category_active">TV Shows</span>
}
render(){
const loader = <div className="loader">Loading ...<br></br></div>;
return(
<InfiniteScroll
pageStart={0}
loadMore={this.pageForwards.bind(this)}
hasMore={this.state.scrollHasMore}
loader={<Loading />}
initialLoad={this.state.loadResults}>
<MediaQuery minWidth={600}>
<div style={searchRequestCSS.body}>
<div className='backgroundHeader' style={searchRequestCSS.backgroundLargeHeader}>
<div className='pageTitle' style={searchRequestCSS.pageTitle}>
<span style={searchRequestCSS.pageTitleLargeSpan}>Request new content for plex</span>
</div>
<div style={searchRequestCSS.searchLargeContainer}>
<span style={searchRequestCSS.searchIcon}><i className="fa fa-search"></i></span>
<input style={searchRequestCSS.searchLargeBar} type="text" id="search" placeholder="Search for new content..."
onKeyPress={(event) => this._handleQueryKeyPress(event)}
onChange={event => this.updateQueryState(event)}
value={this.state.searchQuery}/>
</div>
</div>
<div id='requestMovieList' ref='requestMovieList' style={searchRequestCSS.requestWrapper}>
<div style={{marginLeft: '30px'}}>
<div style={searchRequestCSS.resultLargeHeader}>{this.state.resultHeader}</div>
<span style={{content: '', display: 'block', width: '2em', borderTop: '2px solid #000,'}}></span>
</div>
<br></br>
{this.state.responseMovieList}
</div>
</div>
</MediaQuery>
<MediaQuery maxWidth={600}>
<div style={searchRequestCSS.body}>
<div className='backgroundHeader' style={searchRequestCSS.backgroundSmallHeader}>
<div className='pageTitle' style={searchRequestCSS.pageTitle}>
<span style={searchRequestCSS.pageTitleSmallSpan}>Request new content</span>
</div>
<div className='box' style={searchRequestCSS.box}>
<div style={searchRequestCSS.searchSmallContainer}>
<span style={searchRequestCSS.searchIcon}><i className="fa fa-search"></i></span>
<input style={searchRequestCSS.searchSmallBar} type="text" id="search" placeholder="Search for new content..."
onKeyPress={(event) => this._handleQueryKeyPress(event)}
onChange={event => this.updateQueryState(event)}
value={this.state.searchQuery}/>
</div>
</div>
</div>
<div id='requestMovieList' ref='requestMovieList' style={searchRequestCSS.requestWrapper}>
<span style={searchRequestCSS.resultSmallHeader}>{this.state.resultHeader}</span>
<br></br><br></br>
{this.state.responseMovieList}
</div>
</div>
</MediaQuery>
</InfiniteScroll>
)
}
// <form style={searchRequestCSS.controls}>
// <label style={searchRequestCSS.withData}>
// <div style={searchRequestCSS.sortOptions}>Discover</div>
// </label>
// </form>
// <form style={searchRequestCSS.controls}>
// <label style={searchRequestCSS.withData}>
// <select style={searchRequestCSS.sortOptions}>
// <option value="discover">All</option>
// <option value="nowplaying">Movies</option>
// <option value="nowplaying">TV Shows</option>
// </select>
// </label>
// </form>
}
export default SearchRequest;

View File

@@ -0,0 +1,92 @@
import React from 'react';
import LoginForm from './LoginForm/LoginForm.jsx';
import { Provider } from 'react-redux';
import store from '../redux/store.jsx';
import { getCookie } from '../Cookie.jsx';
import { fetchJSON } from '../http.jsx';
import Sidebar from './Sidebar.jsx';
import AdminRequestInfo from './AdminRequestInfo.jsx';
import adminCSS from '../styles/adminComponent.jsx'
class AdminComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
requested_objects: '',
}
this.updateHandler = this.updateHandler.bind(this)
}
// Fetches all requested elements and updates the state with response
componentWillMount() {
this.fetchRequestedItems()
}
fetchRequestedItems() {
fetchJSON('https://apollo.kevinmidboe.com/api/v1/plex/requests/all', 'GET')
.then(result => {
this.setState({
requested_objects: result.results.reverse()
})
})
}
updateHandler() {
this.fetchRequestedItems()
}
// Displays loginform if not logged in and passes response from
// api call to sidebar and infoPanel through props
verifyLoggedIn() {
const logged_in = getCookie('logged_in');
if (!logged_in) {
return <LoginForm />
}
let selectedRequest = undefined;
let listItemSelected = undefined;
const requestParam = this.props.match.params.request;
if (requestParam && this.state.requested_objects !== '') {
selectedRequest = this.state.requested_objects[requestParam]
listItemSelected = requestParam;
}
return (
<div>
<div style={adminCSS.selectedObjectPanel}>
<AdminRequestInfo
selectedRequest={selectedRequest}
listItemSelected={listItemSelected}
updateHandler = {this.updateHandler}
/>
</div>
<div style={adminCSS.sidebar}>
<Sidebar
requested_objects={this.state.requested_objects}
listItemSelected={listItemSelected}
/>
</div>
</div>
)
}
render() {
return (
<Provider store={store}>
{ this.verifyLoggedIn() }
</Provider>
)
}
}
export default AdminComponent;

View File

@@ -0,0 +1,218 @@
import React, { Component } from 'react';
import { fetchJSON } from '../http.jsx';
import PirateSearch from './PirateSearch.jsx'
// No in use!
import InfoButton from '../buttons/InfoButton.jsx';
// Stylesheets
import requestInfoCSS from '../styles/adminRequestInfo.jsx'
import buttonsCSS from '../styles/buttons.jsx';
String.prototype.capitalize = function() {
return this.charAt(0).toUpperCase() + this.slice(1);
}
class AdminRequestInfo extends Component {
constructor() {
super();
this.state = {
statusValue: '',
movieInfo: undefined,
expandedSummary: false,
}
this.requestInfo = '';
}
componentWillReceiveProps(props) {
this.requestInfo = props.selectedRequest;
this.state.statusValue = this.requestInfo.status;
this.state.expandedSummary = false;
this.fetchIteminfo()
}
userAgent(agent) {
if (agent) {
try {
return agent.split(" ")[1].replace(/[\(\;]/g, '');
}
catch(e) {
return agent;
}
}
return '';
}
generateStatusDropdown() {
return (
<select onChange={ event => this.updateRequestStatus(event) } value={this.state.statusValue}>
<option value='requested'>Requested</option>
<option value='downloading'>Downloading</option>
<option value='downloaded'>Downloaded</option>
</select>
)
}
updateRequestStatus(event) {
const eventValue = event.target.value;
const itemID = this.requestInfo.id;
const apiData = {
type: this.requestInfo.type,
status: eventValue,
}
fetchJSON('https://apollo.kevinmidboe.com/api/v1/plex/request/' + itemID, 'PUT', apiData)
.then((response) => {
console.log('Response, updateRequestStatus: ', response)
this.props.updateHandler()
})
}
generateStatusIndicator(status) {
switch (status) {
case 'requested':
// Yellow
return 'linear-gradient(to right, rgb(63, 195, 243) 0, rgb(63, 195, 243) 10px, #fff 4px, #fff 100%) no-repeat'
case 'downloading':
// Blue
return 'linear-gradient(to right, rgb(255, 225, 77) 0, rgb(255, 225, 77) 10px, #fff 4px, #fff 100%) no-repeat'
case 'downloaded':
// Green
return 'linear-gradient(to right, #39aa56 0, #39aa56 10px, #fff 4px, #fff 100%) no-repeat'
default:
return 'linear-gradient(to right, grey 0, grey 10px, #fff 4px, #fff 100%) no-repeat'
}
}
generateTypeIcon(type) {
if (type === 'show')
return (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="2" y="7" width="20" height="15" rx="2" ry="2"></rect><polyline points="17 2 12 7 7 2"></polyline></svg>
)
else if (type === 'movie')
return (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="2" y="2" width="20" height="20" rx="2.18" ry="2.18"></rect><line x1="7" y1="2" x2="7" y2="22"></line><line x1="17" y1="2" x2="17" y2="22"></line><line x1="2" y1="12" x2="22" y2="12"></line><line x1="2" y1="7" x2="7" y2="7"></line><line x1="2" y1="17" x2="7" y2="17"></line><line x1="17" y1="17" x2="22" y2="17"></line><line x1="17" y1="7" x2="22" y2="7"></line></svg>
)
else
return (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="12"></line><line x1="12" y1="16" x2="12" y2="16"></line></svg>
)
}
toggleSummmaryLength() {
this.setState({
expandedSummary: !this.state.expandedSummary
})
}
generateSummary() {
// { this.state.movieInfo != undefined ? this.state.movieInfo.summary : 'Loading...' }
const info = this.state.movieInfo;
if (info !== undefined) {
const summary = this.state.movieInfo.summary
const summary_short = summary.slice(0, 180);
return (
<div>
<span><b>Matched: </b> {String(info.matchedInPlex)}</span> <br/>
<span><b>Rating: </b> {info.rating}</span> <br/>
<span><b>Popularity: </b> {info.popularity}</span> <br/>
{
(summary.length > 180 && this.state.expandedSummary === false) ?
<span><b>Summary: </b> { summary_short }<span onClick = {() => this.toggleSummmaryLength()}>... <span style={{color: 'blue', cursor: 'pointer'}}>Show more</span></span></span>
:
<span><b>Summary: </b> { summary }<span onClick = {() => this.toggleSummmaryLength()}><span style={{color: 'blue', cursor: 'pointer'}}> Show less</span></span></span>
}
</div>
)
} else {
return <span>Loading...</span>
}
}
requested_by_user(request_user) {
if (request_user === 'NULL')
return undefined
return (
<span><b>Requested by:</b> {request_user}</span>
)
}
fetchIteminfo() {
const itemID = this.requestInfo.id;
const type = this.requestInfo.type;
fetchJSON('https://apollo.kevinmidboe.com/api/v1/tmdb/' + itemID +'&type='+type, 'GET')
.then((response) => {
console.log('Response, getInfo:', response)
this.setState({
movieInfo: response
});
console.log(this.state.movieInfo)
})
}
displayInfo() {
const request = this.props.selectedRequest;
if (request) {
requestInfoCSS.info.background = this.generateStatusIndicator(request.status);
return (
<div style={requestInfoCSS.wrapper}>
<div style={requestInfoCSS.stick}>
<span style={requestInfoCSS.title}> {request.title} {request.year}</span>
<span style={{marginLeft: '2em'}}>
<span style={requestInfoCSS.type_icon}>{this.generateTypeIcon(request.type)}</span>
{/*<span style={style.type_text}>{request.type.capitalize()}</span> <br />*/}
</span>
</div>
<div style={requestInfoCSS.info}>
<div style={requestInfoCSS.info_poster}>
<img src={'https://image.tmdb.org/t/p/w185' + request.poster_path} style={requestInfoCSS.image} alt='Movie poster image'></img>
</div>
<div style={requestInfoCSS.info_request}>
<h3 style={requestInfoCSS.info_request_header}>Request info</h3>
<span><b>status:</b>{ request.status }</span><br />
<span><b>ip:</b>{ request.ip }</span><br />
<span><b>user_agent:</b>{ this.userAgent(request.user_agent) }</span><br />
<span><b>request_date:</b>{ request.requested_date}</span><br />
{ this.requested_by_user(request.requested_by) }<br />
{ this.generateStatusDropdown() }<br />
</div>
<div style={requestInfoCSS.info_movie}>
<h3 style={requestInfoCSS.info_movie_header}>Movie info</h3>
{ this.generateSummary() }
</div>
</div>
<PirateSearch style={requestInfoCSS.search} name={request.title} />
</div>
)
}
}
render() {
return (
<div>{this.displayInfo()}</div>
);
}
}
export default AdminRequestInfo;

View File

@@ -0,0 +1,66 @@
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { login } from '../../redux/reducer.jsx';
class LoginForm extends Component {
constructor(props) {
super(props);
this.state = {};
this.onSubmit = this.onSubmit.bind(this);
}
render() {
let {email, password} = this.state;
let {isLoginPending, isLoginSuccess, loginError} = this.props;
return (
<form name="loginForm" onSubmit={this.onSubmit}>
<div className="form-group-collection">
<div className="form-group">
<label>Email:</label>
<input type="" name="email" onChange={e => this.setState({email: e.target.value})} value={email}/>
</div>
<div className="form-group">
<label>Password:</label>
<input type="password" name="password" onChange={e => this.setState({password: e.target.value})} value={password}/>
</div>
</div>
<input type="submit" value="Login" />
<div className="message">
{ isLoginPending && <div>Please wait...</div> }
{ isLoginSuccess && <div>Success.</div> }
{ loginError && <div>{loginError.message}</div> }
</div>
</form>
)
}
onSubmit(e) {
e.preventDefault();
let { email, password } = this.state;
this.props.login(email, password);
this.setState({
email: '',
password: ''
});
}
}
const mapStateToProps = (state) => {
return {
isLoginPending: state.isLoginPending,
isLoginSuccess: state.isLoginSuccess,
loginError: state.loginError
};
}
const mapDispatchToProps = (dispatch) => {
return {
login: (email, password) => dispatch(login(email, password))
};
}
export default connect(mapStateToProps, mapDispatchToProps)(LoginForm);

View File

@@ -0,0 +1,95 @@
import React, { Component } from 'react';
import { fetchJSON } from '../http.jsx';
// Components
import TorrentTable from './TorrentTable.jsx'
// Stylesheets
import btnStylesheet from '../styles/buttons.jsx';
// Interactive button
import Interactive from 'react-interactive';
import Loading from '../images/loading.jsx'
class PirateSearch extends Component {
constructor() {
super();
this.state = {
torrentResponse: undefined,
name: '',
loading: null,
showButton: true,
}
}
componentWillReceiveProps(props) {
if (props.name != this.state.name) {
this.setState({
torrentResponse: undefined,
showButton: true,
})
}
}
searchTheBay() {
const query = this.props.name;
const type = this.props.type;
this.setState({
showButton: false,
loading: <Loading />,
})
fetchJSON('https://apollo.kevinmidboe.com/api/v1/pirate/search?query='+query+'&type='+type, 'GET')
// fetchJSON('http://localhost:31459/api/v1/pirate/search?query='+query+'&type='+type, 'GET')
.then((response) => {
console.log('this is the first response: ', response)
if (response.success === true) {
this.setState({
torrentResponse: response.torrents,
loading: null,
})
}
else {
console.error(response.message)
}
})
.catch((error) => {
console.error(error);
this.setState({
showButton: true,
})
})
}
render() {
btnStylesheet.submit.top = '50%'
btnStylesheet.submit.position = 'absolute'
btnStylesheet.submit.marginLeft = '-75px'
return (
<div>
{ this.state.showButton ?
<div style={{textAlign:'center'}}>
<Interactive
as='button'
onClick={() => {this.searchTheBay()}}
style={btnStylesheet.submit}
focus={btnStylesheet.submit_hover}
hover={btnStylesheet.submit_hover}>
<span style={{whiteSpace: 'nowrap'}}>Search for torrents</span>
</Interactive>
</div>
: null }
{ this.state.loading }
<TorrentTable response={this.state.torrentResponse} />
</div>
)
}
}
export default PirateSearch

View File

@@ -0,0 +1,247 @@
import React, { Component } from 'react';
import { Link } from 'react-router-dom';
import Interactive from 'react-interactive';
import sidebarCSS from '../styles/adminSidebar.jsx'
class SidebarComponent extends Component {
constructor(props){
super(props)
// Constructor with states holding the search query and the element of reponse.
this.state = {
filterValue: '',
filterQuery: '',
requestItemsToBeDisplayed: [],
listItemSelected: '',
height: '0',
}
this.updateWindowDimensions = this.updateWindowDimensions.bind(this);
}
// Where we wait for api response to be delivered from parent through props
componentWillReceiveProps(props) {
this.state.listItemSelected = props.listItemSelected;
this.displayRequestedElementsInfo(props.requested_objects);
}
componentDidMount() {
this.updateWindowDimensions();
window.addEventListener('resize', this.updateWindowDimensions);
}
componentWillUnmount() {
window.removeEventListener('resize', this.updateWindowDimensions);
}
updateWindowDimensions() {
this.setState({ height: window.innerHeight });
}
// Inputs a date and returns a text string that matches how long it was since
convertDateToDaysSince(date) {
var oneDay = 24*60*60*1000;
var firstDate = new Date(date);
var secondDate = new Date();
var diffDays = Math.round(Math.abs((firstDate.getTime() - secondDate.getTime()) / oneDay));
switch (diffDays) {
case 0:
return 'Today';
case 1:
return '1 day ago'
default:
return diffDays + ' days ago'
}
}
// Called from our dropdown, receives a filter string and checks it with status field
// of our request objects.
filterItems(filterValue) {
let filteredRequestElements = this.props.requested_objects.map((item, index) => {
if (item.status === filterValue || filterValue === 'all')
return this.generateListElements(index, item);
})
this.setState({
requestItemsToBeDisplayed: filteredRequestElements,
filterValue: filterValue,
})
}
// Updates the internal state of the query filter and updates the list to only
// display names matching the query. This is real-time filtering.
updateFilterQuery(event) {
const query = event.target.value;
let filteredByQuery = this.props.requested_objects.map((item, index) => {
if (item.title.toLowerCase().indexOf(query.toLowerCase()) != -1)
return this.generateListElements(index, item);
})
this.setState({
requestItemsToBeDisplayed: filteredByQuery,
filterQuery: query,
});
}
generateFilterSearch() {
return (
<div style={sidebarCSS.searchSidebar}>
<div style={sidebarCSS.searchInner}>
<input
type="text"
id="search"
style={sidebarCSS.searchTextField}
placeholder="Search requested items"
onChange={event => this.updateFilterQuery(event)}
value={this.state.filterQuery}/>
<span>
<svg id="icon-search" style={sidebarCSS.searchIcon} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 15 15">
<g id="search">
<circle style={sidebarCSS.searchSVGIcon} cx="6.055" cy="5.805" r="5.305"></circle>
<path style={sidebarCSS.searchSVGIcon} d="M9.847 9.727l4.166 4.773"></path>
</g>
</svg>
</span>
</div>
</div>
)
}
generateNav() {
let filterValue = this.state.filterValue;
return (
<nav style={sidebarCSS.sidebar_navbar_underline}>
<ul style={sidebarCSS.ulFilterSelectors}>
<li>
<span style={sidebarCSS.aFilterSelectors} onClick = { event => this.filterItems('all') }>All</span>
{ (filterValue === 'all' || filterValue === '') && <span style={sidebarCSS.spanFilterSelectors}></span> }
</li>
<li>
<span style={sidebarCSS.aFilterSelectors} onClick = { event => this.filterItems('requested') }>Requested</span>
{ filterValue === 'requested' && <span style={sidebarCSS.spanFilterSelectors}></span> }
</li>
<li>
<span style={sidebarCSS.aFilterSelectors} onClick = { event => this.filterItems('downloading') }>Downloading</span>
{ filterValue === 'downloading' && <span style={sidebarCSS.spanFilterSelectors}></span> }
</li>
<li>
<span style={sidebarCSS.aFilterSelectors} onClick = { event => this.filterItems('downloaded') }>Downloaded</span>
{ filterValue === 'downloaded' && <span style={sidebarCSS.spanFilterSelectors}></span> }
</li>
</ul>
</nav>
)
}
generateBody(cards) {
let style = sidebarCSS.ulCard;
style.maxHeight = this.state.height - 160;
return (
<ul style={style}>
{ cards }
</ul>
)
}
generateListElements(index, item) {
let statusBar;
switch (item.status) {
case 'requested':
// Yellow
statusBar = { background: 'linear-gradient(to right, rgb(63, 195, 243) 0, rgb(63, 195, 243) 4px, #fff 4px, #fff 100%) no-repeat' }
break;
case 'downloading':
// Blue
statusBar = { background: 'linear-gradient(to right, rgb(255, 225, 77) 0, rgb(255, 225, 77) 4px, #fff 4px, #fff 100%) no-repeat' }
break;
case 'downloaded':
// Green
statusBar = { background: 'linear-gradient(to right, #39aa56 0, #39aa56 4px, #fff 4px, #fff 100%) no-repeat' }
break;
default:
statusBar = { background: 'linear-gradient(to right, grey 0, grey 4px, #fff 4px, #fff 100%) no-repeat' }
}
statusBar.listStyleType = 'none';
return (
<Link style={sidebarCSS.link} to={{ pathname: '/admin/'+String(index)}} key={index}>
<li style={statusBar}>
<Interactive
as='div'
style={ (index != this.state.listItemSelected) ? sidebarCSS.card : sidebarCSS.cardSelected }
hover={sidebarCSS.cardSelected}
focus={sidebarCSS.cardSelected}
active={sidebarCSS.cardSelected}>
<h2 style={sidebarCSS.titleCard}>
<span>{ item.title }</span>
</h2>
<p style={sidebarCSS.pCard}>
<span>Requested:
<time>
&nbsp;{ this.convertDateToDaysSince(item.requested_date) }
</time>
</span>
</p>
</Interactive>
</li>
</Link>
)
}
// This is our main loader that gets called when we receive api response through props from parent
displayRequestedElementsInfo(requested_objects) {
let requestedElement = requested_objects.map((item, index) => {
if (['requested', 'downloading', 'downloaded'].indexOf(this.state.filterValue) != -1) {
if (item.status === this.state.filterValue){
return this.generateListElements(index, item);
}
}
else if (this.state.filterQuery !== '') {
if (item.name.toLowerCase().indexOf(this.state.filterQuery.toLowerCase()) != -1)
return this.generateListElements(index, item);
}
else
return this.generateListElements(index, item);
})
this.setState({
requestItemsToBeDisplayed: this.generateBody(requestedElement)
})
}
render() {
// if (typeof InstallTrigger !== 'undefined')
// bodyCSS.width = '-moz-min-content';
return (
<div>
<h1 style={sidebarCSS.header}>Requested items</h1>
{ this.generateFilterSearch() }
{ this.generateNav() }
<div key='requestedTable' style={sidebarCSS.body}>
{ this.state.requestItemsToBeDisplayed }
</div>
</div>
);
}
}
export default SidebarComponent;

View File

@@ -0,0 +1,209 @@
import React, { Component } from 'react';
import { fetchJSON } from '../http.jsx';
import torrentTableCSS from '../styles/adminTorrentTable.jsx';
class TorrentTable extends Component {
constructor() {
super();
this.state = {
torrentResponse: [],
listElements: undefined,
showTable: false,
filterQuery: '',
sortValue: 'name',
sortDesc: true,
}
this.UNITS = ['B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
}
componentWillReceiveProps(props) {
if (props.response !== undefined && props.response !== this.state.torrentResponse) {
console.log('not called', props)
this.setState({
torrentResponse: props.response,
showTable: true,
})
} else {
this.setState({
showTable: false,
})
}
}
// BORROWED FROM GITHUB user sindresorhus
// Link to repo: https://github.com/sindresorhus/pretty-bytes
convertSizeToHumanSize(num) {
if (!Number.isFinite(num)) {
return num
// throw new TypeError(`Expected a finite number, got ${typeof num}: ${num}`);
}
const neg = num < 0;
if (neg) {
num = -num;
}
if (num < 1) {
return (neg ? '-' : '') + num + ' B';
}
const exponent = Math.min(Math.floor(Math.log10(num) / 3), this.UNITS.length - 1);
const numStr = Number((num / Math.pow(1000, exponent)).toPrecision(3));
const unit = this.UNITS[exponent];
return (neg ? '-' : '') + numStr + ' ' + unit;
}
convertHumanSizeToBytes(string) {
const [numStr, unit] = string.split(' ');
if (this.UNITS.indexOf(unit) === -1) {
return string
}
const exponent = this.UNITS.indexOf(unit) * 3
return numStr * (Math.pow(10, exponent))
}
sendToDownload(magnet) {
const apiData = {
magnet: magnet,
}
fetchJSON('https://apollo.kevinmidboe.com/api/v1/pirate/add', 'POST', apiData)
// fetchJSON('http://localhost:31459/api/v1/pirate/add', 'POST', apiData)
.then((response) => {
console.log('Response, addMagnet: ', response)
// TODO Display the feedback in a notification component (text, status)
})
}
// Updates the internal state of the query filter and updates the list to only
// display names matching the query. This is real-time filtering.
updateFilterQuery(event) {
const query = event.target.value;
let filteredByQuery = this.props.response.map((item, index) => {
if (item.name.toLowerCase().indexOf(query.toLowerCase()) != -1)
return item
})
this.setState({
torrentResponse: filteredByQuery,
filterQuery: query,
});
}
sortTable(col) {
let direction = this.state.sortDesc;
if (col === this.state.sortValue)
direction = !direction;
else
direction = true
let sortedItems = this.state.torrentResponse.sort((a,b) => {
// This is so we also can sort string that only contain numbers
let valueA = isNaN(a[col]) ? a[col] : parseInt(a[col])
let valueB = isNaN(b[col]) ? b[col] : parseInt(b[col])
valueA = (col == 'size') ? this.convertHumanSizeToBytes(valueA) : valueA
valueB = (col == 'size') ? this.convertHumanSizeToBytes(valueB) : valueB
if (direction)
return valueA<valueB? 1:valueA>valueB?-1:0;
else
return valueA>valueB? 1:valueA<valueB?-1:0;
})
this.setState({
torrentResponse: sortedItems,
sortDesc: direction,
sortValue: col,
})
}
generateFilterSearch() {
return (
<div style={torrentTableCSS.searchSidebar}>
<div style={torrentTableCSS.searchInner}>
<input
type="text"
id="search"
style={torrentTableCSS.searchTextField}
placeholder="Filter torrents by query"
onChange={event => this.updateFilterQuery(event)}
value={this.state.filterQuery}/>
<span>
<svg id="icon-search" style={torrentTableCSS.searchIcon} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 15 15">
<g id="search">
<circle style={torrentTableCSS.searchSVGIcon} cx="6.055" cy="5.805" r="5.305"></circle>
<path style={torrentTableCSS.searchSVGIcon} d="M9.847 9.727l4.166 4.773"></path>
</g>
</svg>
</span>
</div>
</div>
)
}
generateListElements() {
let listElements = this.state.torrentResponse.map((item, index) => {
if (item !== undefined) {
let title = item.name
let size = this.convertSizeToHumanSize(item.size)
return (
<tr key={index} style={torrentTableCSS.bodyCol}>
<td>{ item.name }</td>
<td>{ item.uploader }</td>
<td>{ size }</td>
<td>{ item.seed }</td>
<td><button onClick = { event => this.sendToDownload(item.magnet) }>Send to download</button></td>
</tr>
)
}
})
return listElements
}
render() {
return (
<div style= { this.state.showTable ? null : {display: 'none'}}>
{ this.generateFilterSearch() }
<table style={torrentTableCSS.table} cellSpacing="0" cellPadding="0">
<thead>
<tr>
<th style={torrentTableCSS.col} onClick = {event => this.sortTable('name') }>
Title
<svg style={ ( this.state.sortDesc && this.state.sortValue == 'name' ) ? null : {transform: 'rotate(180deg)'} } height="15" viewBox="0 3.5 10 13" version="1.1" width="25" aria-hidden="true"><path fillRule="evenodd" d="M10 10l-1.5 1.5L5 7.75 1.5 11.5 0 10l5-5z"></path></svg>
</th>
<th style={torrentTableCSS.col} onClick = {event => this.sortTable('uploader') }>
Uploader
<svg style={ ( this.state.sortDesc && this.state.sortValue == 'uploader' ) ? null : {transform: 'rotate(180deg)'} } height="15" viewBox="0 3.5 10 13" version="1.1" width="25" aria-hidden="true"><path fillRule="evenodd" d="M10 10l-1.5 1.5L5 7.75 1.5 11.5 0 10l5-5z"></path></svg>
</th>
<th style={torrentTableCSS.col} onClick = {event => this.sortTable('size') }>
Size
<svg style={ ( this.state.sortDesc && this.state.sortValue == 'size' ) ? null : {transform: 'rotate(180deg)'} } height="15" viewBox="0 3.5 10 13" version="1.1" width="25" aria-hidden="true"><path fillRule="evenodd" d="M10 10l-1.5 1.5L5 7.75 1.5 11.5 0 10l5-5z"></path></svg>
</th>
<th style={torrentTableCSS.col} onClick = {event => this.sortTable('seed') }>
Seeds
<svg style={ ( this.state.sortDesc && this.state.sortValue == 'seed' ) ? null : {transform: 'rotate(180deg)'} } height="15" viewBox="0 3.5 10 13" version="1.1" width="25" aria-hidden="true"><path fillRule="evenodd" d="M10 10l-1.5 1.5L5 7.75 1.5 11.5 0 10l5-5z"></path></svg>
</th>
<th style={torrentTableCSS.col}>Magnet</th>
</tr>
</thead>
<tbody>
{this.generateListElements()}
</tbody>
</table>
</div>
)
}
}
export default TorrentTable;

View File

@@ -0,0 +1,52 @@
import React, { Component } from 'react';
import Interactive from 'react-interactive';
import buttonsCSS from '../styles/buttons.jsx';
class InfoButton extends Component {
constructor(props) {
super(props);
if (props) {
this.state = {
id: props.id,
type: props.type,
}
}
}
componentWillReceiveProps(props) {
this.setState({
id: props.id,
type: props.type,
})
}
getTMDBLink() {
const id = this.state.id;
const type = this.state.type;
if (type === 'movie')
return 'https://www.themoviedb.org/movie/' + id
else if (type === 'show')
return 'https://www.themoviedb.org/tv/' + id
}
render() {
return (
<a href={this.getTMDBLink()}>
<Interactive
as='button'
hover={buttonsCSS.info_hover}
focus={buttonsCSS.info_hover}
style={buttonsCSS.info}>
<span>More info</span>
</Interactive>
</a>
);
}
}
export default InfoButton;

View File

@@ -0,0 +1,22 @@
import React from 'react';
class RequestButton extends React.Component {
constructor() {
super();
this.state = {textColor: 'white'};
}
render() {
return (
<Text
style={{color: this.state.textColor}}
onEnter={() => this.setState({textColor: 'red'})}
onExit={() => this.setState({textColor: 'white'})}>
This text will turn red when you look at it.
</Text>
);
}
}
export default RequestButton;

View File

@@ -0,0 +1,53 @@
import React from 'react';
import { getCookie } from './Cookie.jsx';
// class http {
// dispatch(obj) {
// console.log(obj);
// }
function checkStatus(response) {
const hasError = (response.status < 200 || response.status >= 300)
if (hasError) {
throw response.text();
}
return response;
}
function parseJSON(response) { return response.json(); }
// *
// * Retrieve search results from tmdb with added seasoned information.
// * @param {String} uri query you want to search for
// * @param {Number} page representing pagination of results
// * @returns {Promise} succeeds if results were found
// fetchSearch(uri) {
// fetch(uri, {
// method: 'GET',
// headers: {
// 'authorization': getCookie('token')
// },
// })
// .then(response => {
// });
// }
// }
// export default http;
export function fetchJSON(url, method, data) {
return fetch(url, {
method: method,
headers: new Headers({
'Content-Type': 'application/json',
'authorization': getCookie('token'),
'loggedinuser': getCookie('loggedInUser'),
}),
body: JSON.stringify(data)
}).then(checkStatus).then(parseJSON);
}

View File

@@ -0,0 +1,34 @@
import React from 'react';
function Loading() {
return (
<div style={{textAlign: 'center'}}>
<svg version="1.1"
style={{height: '75px'}}
xmlns="http://www.w3.org/2000/svg"
x="0px"
y="0px"
viewBox="0 0 80 80">
<path
fill="#e9a131"
d="M40,72C22.4,72,8,57.6,8,40C8,22.4,
22.4,8,40,8c17.6,0,32,14.4,32,32c0,1.1-0.9,2-2,2
s-2-0.9-2-2c0-15.4-12.6-28-28-28S12,24.6,12,40s12.6,
28,28,28c1.1,0,2,0.9,2,2S41.1,72,40,72z">
<animateTransform
attributeType="xml"
attributeName="transform"
type="rotate"
from="0 40 40"
to="360 40 40"
dur="1.0s"
repeatCount="indefinite"/>
</path>
</svg>
</div>
)
}
export default Loading;

View File

@@ -0,0 +1,109 @@
import { setCookie } from '../Cookie.jsx';
const SET_LOGIN_PENDING = 'SET_LOGIN_PENDING';
const SET_LOGIN_SUCCESS = 'SET_LOGIN_SUCCESS';
const SET_LOGIN_ERROR = 'SET_LOGIN_ERROR';
export function login(email, password) {
return dispatch => {
dispatch(setLoginPending(true));
dispatch(setLoginSuccess(false));
dispatch(setLoginError(null));
callLoginApi(email, password, error => {
dispatch(setLoginPending(false));
if (!error) {
dispatch(setLoginSuccess(true));
} else {
dispatch(setLoginError(error));
}
});
}
}
function setLoginPending(isLoginPending) {
return {
type: SET_LOGIN_PENDING,
isLoginPending
};
}
function setLoginSuccess(isLoginSuccess) {
return {
type: SET_LOGIN_SUCCESS,
isLoginSuccess
};
}
function setLoginError(loginError) {
return {
type: SET_LOGIN_ERROR,
loginError
}
}
function callLoginApi(username, password, callback) {
Promise.resolve()
fetch('https://apollo.kevinmidboe.com/api/v1/user/login', {
method: 'POST',
headers: {
'Content-type': 'application/json'
},
body: JSON.stringify({
username: username,
password: password,
})
})
.then(response => {
switch (response.status) {
case 200:
response.json()
.then((data) => {
if (data.success === true) {
let token = data.token;
setCookie('token', token, 10);
setCookie('logged_in', true, 10);
setCookie('loggedInUser', username, 10);
window.location.reload();
}
return callback(null);
})
case 401:
return callback(new Error(response.statusText));
}
})
.catch(error => {
return callback(new Error('Invalid username and password'));
});
}
export default function reducer(state = {
isLoginSuccess: false,
isLoginPending: false,
loginError: null
}, action) {
switch (action.type) {
case SET_LOGIN_PENDING:
return Object.assign({}, state, {
isLoginPending: action.isLoginPending
});
case SET_LOGIN_SUCCESS:
return Object.assign({}, state, {
isLoginSuccess: action.isLoginSuccess
});
case SET_LOGIN_ERROR:
return Object.assign({}, state, {
loginError: action.loginError
});
default:
return state;
}
}

View File

@@ -0,0 +1,7 @@
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import logger from 'redux-logger';
import reducer from './reducer.jsx';
const store = createStore(reducer, {}, applyMiddleware(thunk, logger));
export default store;

View File

@@ -0,0 +1,16 @@
export default {
sidebar: {
float: 'left',
width: '18%',
minWidth: '250px',
fontFamily: '"Open Sans", sans-serif',
fontSize: '14px',
borderRight: '2px solid #f2f2f2',
},
selectedObjectPanel: {
width: '80%',
float: 'right',
fontFamily: '"Open Sans", sans-serif',
marginTop: '1em',
}
}

View File

@@ -0,0 +1,58 @@
export default {
wrapper: {
width: '100%',
},
stick: {
marginBottom: '1em',
},
title: {
fontSize: '2em',
},
image: {
width: '105px',
borderRadius: '4px',
},
info: {
paddingTop: '1em',
paddingBottom: '0.5em',
marginRight: '2em',
backgroundColor: 'white',
border: '1px solid #d0d0d0',
borderRadius: '2px',
display: 'flex',
},
type_icon: {
marginLeft: '-0.2em',
marginRight: '0.7em',
},
type_text: {
verticalAlign: 'super',
},
info_poster: {
marginLeft: '2em',
flex: '0 1 10%'
},
info_request: {
flex: '0 1 auto'
},
info_request_header: {
margin: '0',
marginBottom: '0.5em',
},
info_movie: {
maxWidth: '70%',
marginLeft: '1em',
flex: '0 1 auto',
},
info_movie_header: {
margin: '0',
marginBottom: '0.5em',
}
}

View File

@@ -0,0 +1,153 @@
export default {
header: {
textAlign: 'center',
},
body: {
backgroundColor: 'white',
},
parentElement: {
display: 'inline-block',
width: '100%',
border: '1px solid grey',
borderRadius: '2px',
padding: '4px',
margin: '4px',
marginLeft: '4px',
backgroundColor: 'white',
},
parentElement_hover: {
backgroundColor: '#f8f8f8',
pointer: 'hand',
},
parentElement_active: {
textDecoration: 'none',
},
parentElement_selected: {
display: 'inline-block',
width: '100%',
border: '1px solid grey',
borderRadius: '2px',
padding: '4px',
margin: '4px 0px 4px 4px',
marginLeft: '10px',
backgroundColor: 'white',
},
title: {
maxWidth: '65%',
display: 'inline-flex',
},
link: {
color: 'black',
textDecoration: 'none',
},
rightContainer: {
float: 'right',
},
searchSidebar: {
height: '4em',
},
searchInner: {
top: '0',
right: '0',
left: '0',
bottom: '0',
margin: 'auto',
width: '90%',
minWidth: '280px',
height: '30px',
border: '1px solid #d0d0d0',
borderRadius: '4px',
overflow: 'hidden'
},
searchTextField: {
display: 'inline-block',
width: '90%',
padding: '.3em',
verticalAlign: 'middle',
border: 'none',
background: '#fff',
fontSize: '14px',
marginTop: '-7px',
},
searchIcon: {
width: '15px',
height: '16px',
marginRight: '4px',
marginTop: '7px',
},
searchSVGIcon: {
fill: 'none',
stroke: '#9d9d9d',
strokeLinecap: 'round',
strokeLinejoin: 'round',
strokeMiterlimit: '10',
},
ulFilterSelectors: {
borderBottom: '2px solid #f1f1f1',
display: 'flex',
padding: '0',
margin: '0',
listStyle: 'none',
justifyContent: 'space-evenly',
},
aFilterSelectors: {
color: '#3eaaaf',
fontSize: '16px',
cursor: 'pointer',
},
spanFilterSelectors: {
content: '""',
bottom: '-2px',
display: 'block',
width: '100%',
height: '2px',
backgroundColor: '#3eaaaa',
},
ulCard: {
margin: '1em 0 0 0',
padding: '0',
listStyle: 'none',
borderBottom: '.46rem solid #f1f1f',
backgroundColor: '#f1f1f1',
overflow: 'scroll',
},
card: {
padding: '.1em .5em .8em 1.5em',
marginBottom: '.26rem',
height: 'auto',
cursor: 'pointer',
},
cardSelected: {
padding: '.1em .5em .8em 1.5em',
marginBottom: '.26rem',
height: 'auto',
cursor: 'pointer',
backgroundColor: '#f9f9f9',
},
titleCard: {
fontSize: '15px',
fontWeight: '400',
whiteSpace: 'no-wrap',
textDecoration: 'none',
},
pCard: {
margin: '0',
},
}

View File

@@ -0,0 +1,59 @@
export default {
table: {
width: '80%',
marginRight: 'auto',
marginLeft: 'auto',
},
tableHeader: {
},
col: {
cursor: 'pointer',
borderBottom: '1px solid #e0e0e0',
paddingBottom: '0.5em',
textAlign: 'left',
},
bodyCol: {
marginTop: '0.5em',
},
searchSidebar: {
height: '4em',
marginTop: '1em',
},
searchInner: {
top: '0',
right: '0',
left: '0',
bottom: '0',
margin: 'auto',
width: '50%',
minWidth: '280px',
height: '30px',
border: '1px solid #d0d0d0',
borderRadius: '4px',
overflow: 'hidden'
},
searchTextField: {
display: 'inline-block',
width: '95%',
padding: '.3em',
verticalAlign: 'middle',
border: 'none',
background: '#fff',
fontSize: '14px',
marginTop: '-7px',
},
searchIcon: {
width: '15px',
height: '16px',
marginRight: '4px',
marginTop: '7px',
},
searchSVGIcon: {
fill: 'none',
stroke: '#9d9d9d',
strokeLinecap: 'round',
strokeLinejoin: 'round',
strokeMiterlimit: '10',
},
}

View File

@@ -0,0 +1,80 @@
export default {
submit: {
color: '#e9a131',
marginRight: '10px',
backgroundColor: 'white',
border: '#e9a131 2px solid',
borderColor: '#e9a131',
borderRadius: '4px',
textAlign: 'center',
padding: '10px',
minWidth: '100px',
float: 'left',
fontSize: '13px',
fontWeight: '800',
cursor: 'pointer',
},
submit_hover: {
backgroundColor: '#e9a131',
color: 'white',
},
info: {
color: '#00d17c',
marginRight: '10px',
backgroundColor: 'white',
border: '#00d17c 2px solid',
borderRadius: '4px',
textAlign: 'center',
padding: '10px',
minWidth: '100px',
float: 'left',
fontSize: '13px',
fontWeight: '800',
cursor: 'pointer',
},
info_hover: {
backgroundColor: '#00d17c',
color: 'white',
},
edit: {
color: '#4a95da',
marginRight: '10px',
backgroundColor: 'white',
border: '#4a95da 2px solid',
borderRadius: '4px',
textAlign: 'center',
padding: '10px',
minWidth: '100px',
float: 'left',
fontSize: '13px',
fontWeight: '800',
cursor: 'pointer',
},
edit_small: {
color: '#4a95da',
marginRight: '10px',
backgroundColor: 'white',
border: '#4a95da 2px solid',
borderRadius: '4px',
textAlign: 'center',
padding: '4px',
minWidth: '50px',
float: 'left',
fontSize: '13px',
fontWeight: '800',
cursor: 'pointer',
},
edit_hover: {
backgroundColor: '#4a95da',
color: 'white',
},
}

View File

@@ -0,0 +1,24 @@
export default {
bodyDiv: {
display: 'flex',
flexDirection: 'row',
flexWrap: 'wrap',
flexFlow: 'row wrap',
justifyContent: 'space-around',
},
wrappingDiv: {
},
requestPoster: {
height: '150px',
},
infoDiv: {
marginTop: 0,
marginLeft: '10px',
float: 'right',
},
}

View File

@@ -0,0 +1,62 @@
export default {
container: {
maxWidth: '95%',
margin: '0 auto',
minHeight: '230px'
},
title_large: {
color: 'black',
fontSize: '2em',
},
title_small: {
color: 'black',
fontSize: '22px',
},
stats_large: {
fontSize: '0.8em'
},
stats_small: {
marginTop: '5px',
fontSize: '0.8em'
},
posterContainer: {
float: 'left',
zIndex: '3',
position: 'relative',
marginRight: '30px'
},
posterImage: {
border: '2px none',
borderRadius: '2px',
width: '150px'
},
backgroundImage: {
width: '100%'
},
buttons: {
paddingTop: '20px',
},
summary: {
fontSize: '15px',
},
dividerRow: {
width: '100%'
},
itemDivider: {
width: '90%',
borderBottom: '1px solid grey',
margin: '2rem auto'
}
}

View File

@@ -0,0 +1,177 @@
export default {
body: {
fontFamily: "'Open Sans', sans-serif",
backgroundColor: '#f7f7f7',
margin: 0,
padding: 0,
minHeight: '100%',
},
backgroundLargeHeader: {
width: '100%',
minHeight: '180px',
backgroundColor: 'rgb(1, 28, 35)',
// backgroundImage: 'radial-gradient(circle, #004c67 0, #005771 120%)',
zIndex: 1,
marginBottom: '70px'
},
backgroundSmallHeader: {
width: '100%',
minHeight: '120px',
backgroundColor: '#011c23',
zIndex: 1,
marginBottom: '40px'
},
requestWrapper: {
maxWidth: '1200px',
margin: 'auto',
paddingTop: '10px',
backgroundColor: 'white',
position: 'relative',
zIndex: '10',
boxShadow: '0 1px 2px grey',
},
pageTitle: {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
textAlign: 'center',
},
pageTitleLargeSpan: {
color: 'white',
fontSize: '3em',
marginTop: '4vh',
marginBottom: '6vh'
},
pageTitleSmallSpan: {
color: 'white',
fontSize: '2em',
marginTop: '3vh',
marginBottom: '3vh'
},
searchLargeContainer: {
height: '52px',
width: '77%',
paddingLeft: '23%',
backgroundColor: 'white',
boxShadow: 'grey 0px 1px 2px',
},
searchSmallContainer: {
},
searchIcon: {
position: 'absolute',
fontSize: '1.6em',
marginTop: '7px',
color: '#4f5b66',
display: 'block',
},
searchLargeBar: {
width: '50%',
height: '50px',
background: '#ffffff',
border: 'none',
fontSize: '12pt',
float: 'left',
color: '#63717f',
paddingLeft: '40px',
},
searchSmallBar: {
width: '100%',
height: '50px',
background: '#ffffff',
border: 'none',
fontSize: '11pt',
float: 'left',
color: '#63717f',
paddingLeft: '65px',
marginLeft: '-25px',
borderRadius: '5px',
},
// Dropdown for selecting tmdb lists
controls: {
textAlign: 'left',
paddingTop: '8px',
width: '33.3333%',
marginLeft: '0',
marginRight: '0',
},
withData: {
boxSizing: 'border-box',
marginBottom: '0',
display: 'block',
padding: '0',
verticalAlign: 'baseline',
font: 'inherit',
textAlign: 'left',
boxSizing: 'border-box',
},
sortOptions: {
border: '1px solid #000',
maxWidth: '100%',
overflow: 'hidden',
lineHeight: 'normal',
textAlign: 'left',
padding: '4px 12px',
paddingRight: '2rem',
backgroundImage: 'url("data:image/svg+xml;base64,PHN2ZyBpZD0iTGF5ZXJfMSIgZGF0YS1uYW1lPSJMYXllciAxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxOCAxOCI+CiAgPHRpdGxlPmFycm93LWRvd24tbWljcm88L3RpdGxlPgogIDxwb2x5bGluZSBwb2ludHM9IjE0IDQuNjcgOSAxMy4zMyA0IDQuNjciIHN0eWxlPSJmaWxsOiBub25lO3N0cm9rZTogIzAwMDtzdHJva2UtbWl0ZXJsaW1pdDogMTA7c3Ryb2tlLXdpZHRoOiAycHgiLz4KPC9zdmc+Cg==")',
backgroundSize: '18px 18px',
backgroundPosition: 'right 8px center',
backgroundRepeat: 'no-repeat',
width: 'auto',
display: 'inline-block',
outline: 'none',
boxSizing: 'border-box',
fontSize: '15px',
WebkitAppearance: 'none',
MozAppearance: 'none',
appearance: 'none',
},
searchFilterActive: {
color: '#00d17c',
fontSize: '1em',
marginLeft: '10px',
cursor: 'pointer'
},
searchFilterNotActive: {
color: 'white',
fontSize: '1em',
marginLeft: '10px',
cursor: 'pointer'
},
filter: {
color: 'white',
paddingLeft: '40px',
width: '60%',
},
resultLargeHeader: {
color: 'black',
fontSize: '1.6em',
width: '20%',
},
resultSmallHeader: {
paddingLeft: '12px',
color: 'black',
fontSize: '1.4em',
},
}

View File

@@ -2,9 +2,12 @@
<html>
<head>
<meta charset="utf-8">
<link href="https://fonts.googleapis.com/css?family=Open+Sans:300" rel="stylesheet">
<link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.1.0/css/font-awesome.min.css" rel="stylesheet">
<meta name="viewport" content="width=device-width, initial-scale=1.0 maximum-scale=1.0, user-scalable=0">
<title>seasoned Shows</title>
</head>
<body>
<body style='margin: 0'>
<div id="root">
</div>

View File

@@ -2,14 +2,19 @@
* @Author: KevinMidboe
* @Date: 2017-06-01 21:08:55
* @Last Modified by: KevinMidboe
* @Last Modified time: 2017-06-01 21:34:32
* @Last Modified time: 2017-10-20 19:24:52
./client/index.js
which is the webpack entry file
*/
import React from 'react';
import ReactDOM from 'react-dom';
import App from './components/App.jsx';
import { render } from 'react-dom';
import { HashRouter } from 'react-router-dom';
import Root from './Root.jsx';
ReactDOM.render(<App />, document.getElementById('root'));
render((
<HashRouter>
<Root />
</HashRouter>
), document.getElementById('root'));

View File

@@ -6,20 +6,38 @@
"author": "Kevin Midboe",
"license": "MIT",
"scripts": {
"start": "webpack-dev-server"
"start": "webpack-dev-server --open --config webpack.dev.js",
"build": "NODE_ENV=production webpack --config webpack.prod.js",
"build_dev": "webpack --config webpack.dev.js"
},
"dependencies": {
"clean-webpack-plugin": "^0.1.17",
"css-loader": "^1.0.0",
"html-webpack-plugin": "^2.28.0",
"path": "^0.12.7",
"react": "^15.5.4",
"react": "^15.6.1",
"react-burger-menu": "^2.1.6",
"react-dom": "^15.5.4",
"webpack": "^2.6.1",
"webpack-dev-server": "^2.4.5"
"react-infinite-scroller": "^1.0.15",
"react-interactive": "^0.8.1",
"react-notify-toast": "^0.3.2",
"react-redux": "^5.0.6",
"react-responsive": "^1.3.4",
"react-router": "^4.2.0",
"react-router-dom": "^4.2.2",
"redux": "^3.7.2",
"redux-logger": "^3.0.6",
"redux-thunk": "^2.2.0",
"urijs": "^1.18.12",
"webfontloader": "^1.6.28",
"webpack": "^4.0.0",
"webpack-dev-server": "^3.1.11",
"webpack-merge": "^4.1.0"
},
"devDependencies": {
"babel-core": "^6.24.1",
"babel-loader": "^7.0.0",
"babel-preset-env": "^1.5.1",
"babel-core": "^6.26.0",
"babel-loader": "^7.1.2",
"babel-preset-env": "^1.6.0",
"babel-preset-es2015": "^6.24.1",
"babel-preset-react": "^6.24.1"
}

33
client/webpack.common.js Normal file
View File

@@ -0,0 +1,33 @@
/*
* @Author: KevinMidboe
* @Date: 2017-06-01 19:09:16
* @Last Modified by: KevinMidboe
* @Last Modified time: 2017-10-24 21:55:41
*/
const path = require('path');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: {
app: './app/index.js',
},
plugins: [
new CleanWebpackPlugin(['dist']),
new HtmlWebpackPlugin({
template: './app/index.html',
})
],
module: {
loaders: [
{ test: /\.(js|jsx)$/, loader: 'babel-loader', exclude: /node_modules/ },
]
},
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist')
}
};

View File

@@ -1,33 +0,0 @@
/*
* @Author: KevinMidboe
* @Date: 2017-06-01 19:09:16
* @Last Modified by: KevinMidboe
* @Last Modified time: 2017-06-01 22:11:51
*/
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const HtmlWebpackPluginConfig = new HtmlWebpackPlugin({
template: './app/index.html',
filename: 'index.html',
inject: 'body'
})
module.exports = {
entry: './app/index.js',
output: {
path: path.resolve('dist'),
filename: 'index_bundle.js'
},
devServer: {
headers: { "Access-Control-Allow-Origin": "*" }
},
module: {
loaders: [
{ test: /\.js$/, loader: 'babel-loader', exclude: /node_modules/ },
{ test: /\.jsx$/, loader: 'babel-loader', exclude: /node_modules/ }
]
},
plugins: [HtmlWebpackPluginConfig]
}

17
client/webpack.dev.js Normal file
View File

@@ -0,0 +1,17 @@
/*
* @Author: KevinMidboe
* @Date: 2017-06-01 19:09:16
* @Last Modified by: KevinMidboe
* @Last Modified time: 2017-10-24 22:12:52
*/
const merge = require('webpack-merge');
const common = require('./webpack.common.js');
module.exports = merge(common, {
devtool: 'inline-source-map',
devServer: {
contentBase: './dist',
headers: {'Access-Control-Allow-Origin': '*'}
}
});;

28
client/webpack.prod.js Normal file
View File

@@ -0,0 +1,28 @@
/*
* @Author: KevinMidboe
* @Date: 2017-06-01 19:09:16
* @Last Modified by: KevinMidboe
* @Last Modified time: 2017-10-24 22:26:29
*/
const merge = require('webpack-merge');
const UglifyJSPlugin = require('uglifyjs-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const common = require('./webpack.common.js');
var webpack = require('webpack')
module.exports = merge(common, {
plugins: [
new UglifyJSPlugin(),
new HtmlWebpackPlugin({
template: './app/index.html',
title: 'Caching'
}),
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development')
}),
],
output: {
filename: '[name].[chunkhash].js',
}
});

File diff suppressed because it is too large Load Diff

View File

View File

@@ -1,11 +0,0 @@
{
"database": {
"host": "shows.db"
},
"webserver": {
"port": 31459
},
"tmdb": {
"apiKey": "9fa154f5355c37a1b9b57ac06e7d6712"
}
}

View File

@@ -1,60 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# @Author: KevinMidboe
# @Date: 2017-04-12 23:27:51
# @Last Modified by: KevinMidboe
# @Last Modified time: 2017-04-13 16:22:23
import sys, sqlite3, json, os
import env_variables as env
class episode(object):
def __init__(self, id):
self.id = id
self.getVarsFromDB()
def getVarsFromDB(self):
c = sqlite3.connect(env.db_path).cursor()
c.execute('SELECT parent, name, season, episode, video_files, subtitles, trash FROM stray_eps WHERE id = ?', (self.id,))
returnMsg = c.fetchone()
self.parent = returnMsg[0]
self.name = returnMsg[1]
self.season = returnMsg[2]
self.episode = returnMsg[3]
self.video_files = json.loads(returnMsg[4])
self.subtitles = json.loads(returnMsg[5])
self.trash = json.loads(returnMsg[6])
c.close()
self.queries = {
'parent': [env.show_dir, self.parent],
'season': [env.show_dir, self.name, self.name + ' Season ' + "%02d" % self.season],
'episode': [env.show_dir, self.name, self.name + ' Season ' + "%02d" % self.season, \
self.name + ' S' + "%02d" % self.season + 'E' + "%02d" % self.episode],
}
def typeDir(self, dType, create=False, mergeItem=None):
url = '/'.join(self.queries[dType])
if create and not os.path.isdir(url):
os.makedirs(url)
if mergeItem:
return '/'.join([url, str(mergeItem)])
return url
def moveStray(strayId):
ep = episode(strayId)
for item in ep.video_files:
os.rename(ep.typeDir('parent', mergeItem=item[0]), ep.typeDir('episode', mergeItem=item[1], create=True))
for item in ep.subtitles:
os.rename(ep.typeDir('parent', mergeItem=item[0]), ep.typeDir('episode', mergeItem=item[1], create=True))
for item in ep.trash:
os.remove(ep.typeDir('parent', mergeItem=item))
os.rmdir(ep.typeDir('parent'))
if __name__ == '__main__':
moveStray(sys.argv[-1])

View File

@@ -1,19 +0,0 @@
{
"name": "node-api",
"main": "src/webserver/server.js",
"scripts": {
"start": "cross-env SEASONED_CONFIG=conf/development.json NODE_PATH=. node src/webserver/server.js"
},
"dependencies": {
"express": "~4.0.0",
"mongoose": "~3.6.13",
"body-parser": "~1.0.1",
"cross-env": "^3.1.3",
"sqlite": "^2.5.0",
"request": "^2.81.0",
"python-shell": "^0.4.0",
"moviedb": "^0.2.7",
"node-cache": "^4.1.1",
"request-promise": "^4.2"
}
}

View File

@@ -1 +0,0 @@
So to get people to sign up for a account could be to have them sign up for shows that they can be alerted when are added. They can choose by SMS, twitter, email or maybe newsletter.

View File

@@ -0,0 +1,14 @@
{
"extends": [
"airbnb-base"
],
"rules": {
"indent": ["error", 3],
"prefer-destructuring": 0,
"camelcase": 0,
"import/no-unresolved": 0,
"import/no-extraneous-dependencies": 0,
"object-shorthand": 0,
"comma-dangle": 0
}
}

66
seasoned_api/.gitignore vendored Normal file
View File

@@ -0,0 +1,66 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Typescript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
# - - - - -
# My own gitignore files and folders
shows.db
conf/development.json
# conf/development-prod.json

View File

@@ -0,0 +1,20 @@
{
"database": {
"host": "../shows.db"
},
"webserver": {
"port": 31459
},
"tmdb": {
"apiKey": ""
},
"plex": {
"ip": ""
},
"raven": {
"DSN": ""
},
"authentication": {
"secret": "secret"
}
}

View File

@@ -0,0 +1,20 @@
{
"database": {
"host": ":memory:"
},
"webserver": {
"port": 31400
},
"tmdb": {
"apiKey": "bogus-api-key"
},
"plex": {
"ip": "0.0.0.0"
},
"raven": {
"DSN": ""
},
"authentication": {
"secret": "secret"
}
}

57
seasoned_api/package.json Normal file
View File

@@ -0,0 +1,57 @@
{
"name": "seasoned-api",
"description": "Packages needed to build and commands to run seasoned api node server.",
"license": {
"type": "MIT",
"url": "https://www.opensource.org/licenses/mit-license.php"
},
"main": "webserver/server.js",
"scripts": {
"start": "cross-env SEASONED_CONFIG=conf/development.json PROD=true NODE_PATH=. babel-node src/webserver/server.js",
"test": "cross-env SEASONED_CONFIG=conf/test.json NODE_PATH=. mocha --require @babel/register --recursive test/unit test/system",
"coverage": "cross-env SEASONED_CONFIG=conf/test.json NODE_PATH=. nyc mocha --require @babel/register --recursive test && nyc report --reporter=text-lcov | coveralls",
"lint": "./node_modules/.bin/eslint src/",
"update": "cross-env SEASONED_CONFIG=conf/development.json NODE_PATH=. node src/plex/updateRequestsInPlex.js",
"docs": "yarn apiDocs; yarn classDocs",
"apiDocs": "",
"classDocs": "./script/generate-class-docs.sh"
},
"dependencies": {
"axios": "^0.18.0",
"bcrypt": "^3.0.6",
"body-parser": "~1.18.2",
"cross-env": "~5.1.4",
"express": "~4.16.0",
"express-graphql": "^0.9.0",
"express-reload": "^1.2.0",
"graphql": "^14.5.8",
"jsonwebtoken": "^8.2.0",
"km-moviedb": "^0.2.12",
"node-cache": "^4.1.1",
"node-fetch": "^2.6.0",
"python-shell": "^0.5.0",
"raven": "^2.4.2",
"request": "^2.87.0",
"request-promise": "^4.2",
"sqlite3": "^4.0.0"
},
"devDependencies": {
"@babel/core": "^7.5.5",
"@babel/node": "^7.5.5",
"@babel/preset-env": "^7.5.5",
"@babel/register": "^7.5.5",
"@types/node": "^12.6.8",
"coveralls": "^3.0.5",
"documentation": "^12.0.3",
"eslint": "^4.9.0",
"eslint-config-airbnb-base": "^12.1.0",
"eslint-plugin-import": "^2.8.0",
"istanbul": "^0.4.5",
"mocha": "^6.2.0",
"mocha-lcov-reporter": "^1.3.0",
"nyc": "^11.6.0",
"supertest": "^3.0.0",
"supertest-as-promised": "^4.0.1",
"typescript": "^3.5.3"
}
}

View File

@@ -0,0 +1,43 @@
const path = require('path');
const Field = require('./field.js');
let instance = null;
class Config {
constructor() {
this.location = Config.determineLocation();
this.fields = require(`${this.location}`);
}
static getInstance() {
if (instance == null) {
instance = new Config();
}
return instance;
}
static determineLocation() {
return path.join(__dirname, '..', '..', process.env.SEASONED_CONFIG);
}
get(section, option) {
if (this.fields[section] === undefined || this.fields[section][option] === undefined) {
throw new Error(`Filed "${section} => ${option}" does not exist.`);
}
const field = new Field(this.fields[section][option]);
if (field.value === '') {
const envField = process.env[['SEASONED', section.toUpperCase(), option.toUpperCase()].join('_')];
if (envField !== undefined && envField.length !== 0) { return envField; }
}
if (field.value === undefined) {
throw new Error(`${section} => ${option} is empty.`);
}
return field.value;
}
}
module.exports = Config;

View File

@@ -0,0 +1,15 @@
class EnvironmentVariables {
constructor(variables) {
this.variables = variables || process.env;
}
get(variable) {
return this.variables[variable];
}
has(variable) {
return this.get(variable) !== undefined;
}
}
module.exports = EnvironmentVariables;

View File

@@ -0,0 +1,49 @@
const Filters = require('./filters.js');
const EnvironmentVariables = require('./environmentVariables.js');
class Field {
constructor(rawValue, environmentVariables) {
this.rawValue = rawValue;
this.filters = new Filters(rawValue);
this.valueWithoutFilters = this.filters.removeFiltersFromValue();
this.environmentVariables = new EnvironmentVariables(environmentVariables);
}
get value() {
if (this.filters.isEmpty()) {
return this.valueWithoutFilters;
}
if (this.filters.has('base64') && !this.filters.has('env')) {
return Field.base64Decode(this.valueWithoutFilters);
}
if (this.environmentVariables.has(this.valueWithoutFilters) &&
this.environmentVariables.get(this.valueWithoutFilters) === '') {
return undefined;
}
if (!this.filters.has('base64') && this.filters.has('env')) {
if (this.environmentVariables.has(this.valueWithoutFilters)) {
return this.environmentVariables.get(this.valueWithoutFilters);
}
return undefined;
}
if (this.filters.has('env') && this.filters.has('base64')) {
if (this.environmentVariables.has(this.valueWithoutFilters)) {
const encodedEnvironmentVariable = this.environmentVariables.get(this.valueWithoutFilters);
return Field.base64Decode(encodedEnvironmentVariable);
}
return undefined;
}
return this.valueWithoutFilters;
}
static base64Decode(string) {
return new Buffer(string, 'base64').toString('utf-8');
}
}
module.exports = Field;

View File

@@ -0,0 +1,34 @@
class Filters {
constructor(value) {
this.value = value;
this.delimiter = '|';
}
get filters() {
return this.value.split(this.delimiter).slice(0, -1);
}
isEmpty() {
return !this.hasValidType() || this.value.length === 0;
}
has(filter) {
return this.filters.includes(filter);
}
hasValidType() {
return (typeof this.value === 'string');
}
removeFiltersFromValue() {
if (this.hasValidType() === false) {
return this.value;
}
let filtersCombined = this.filters.join(this.delimiter);
filtersCombined += this.filters.length >= 1 ? this.delimiter : '';
return this.value.replace(filtersCombined, '');
}
}
module.exports = Filters;

View File

@@ -1,7 +1,7 @@
const configuration = require('src/config/configuration').getInstance();
const SqliteDatabase = require('src/database/sqliteDatabase');
const database = new SqliteDatabase(configuration.get('database', 'host'));
const database = new SqliteDatabase(configuration.get('database', 'host'));
/**
* This module establishes a connection to the database
* specified in the confgiuration file. It tries to setup
@@ -9,7 +9,6 @@ const database = new SqliteDatabase(configuration.get('database', 'host'));
* If the tables already exists, it simply proceeds.
*/
Promise.resolve()
.then(() => database.connect())
// .then(() => database.setUp());
.then(() => database.setUp());
module.exports = database;

View File

@@ -0,0 +1,86 @@
CREATE TABLE IF NOT EXISTS user (
user_name varchar(127) UNIQUE,
password varchar(127),
email varchar(127) UNIQUE,
admin boolean DEFAULT 0,
primary key (user_name)
);
CREATE TABLE IF NOT EXISTS cache (
key varchar(255),
value blob,
time_to_live INTEGER DEFAULT 60,
created_at DATE DEFAULT (datetime('now','localtime')),
primary key(key)
);
CREATE TABLE IF NOT EXISTS search_history (
id integer,
user_name varchar(127),
search_query varchar(255),
primary key (id),
foreign key(user_name) REFERENCES user(user_name) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS requests(
id TEXT,
title TEXT,
year NUMBER,
poster_path TEXT DEFAULT NULL,
background_path TEXT DEFAULT NULL,
requested_by TEXT,
ip TEXT,
date DATE DEFAULT CURRENT_TIMESTAMP,
status CHAR(25) DEFAULT 'requested' NOT NULL,
user_agent CHAR(255) DEFAULT NULL,
type CHAR(50) DEFAULT 'movie'
);
CREATE TABLE IF NOT EXISTS request(
id int not null,
title text not null,
year int not null,
type char(10) not null,
date timestamp default (strftime('%s', 'now'))
);
CREATE TABLE IF NOT EXISTS stray_eps(
id TEXT UNIQUE,
parent TEXT,
path TEXT,
name TEXT,
season NUMBER,
episode NUMBER,
video_files TEXT,
subtitles TEXT,
trash TEXT,
verified BOOLEAN DEFAULT 0,
primary key(id)
);
CREATE TABLE IF NOT EXISTS shows(
show_names TEXT,
date_added DATE,
date_modified DATE DEFUALT CURRENT_DATE NOT NULL
);
CREATE TABLE IF NOT EXISTS requested_torrent (
magnet TEXT UNIQUE,
torrent_name TEXT,
tmdb_id TEXT
date_added DATE DEFAULT (datetime('now','localtime'))
);
CREATE TABLE IF NOT EXISTS deluge_torrent (
key TEXT UNIQUE,
name TEXT,
progress TEXT,
eta NUMBER,
save_path TEXT,
state TEXT,
paused BOOLEAN,
finished BOOLEAN,
files TEXT,
is_folder BOOLEAN
)

View File

@@ -0,0 +1,4 @@
DROP TABLE IF EXISTS user;
DROP TABLE IF EXISTS search_history;
DROP TABLE IF EXISTS requests;
DROP TABLE IF EXISTS request;

View File

@@ -0,0 +1,119 @@
const fs = require('fs');
const path = require('path');
const sqlite3 = require('sqlite3').verbose();
class SqliteDatabase {
constructor(host) {
this.host = host;
this.connection = new sqlite3.Database(this.host);
this.schemaDirectory = path.join(__dirname, 'schemas');
}
/**
* Connect to the database.
* @returns {Promise} succeeds if connection was established
*/
// connect() {
// let database = ;
// this.connection = database;
// return database;
// }
/**
* Run a SQL query against the database.
* @param {String} sql SQL query
* @param {Array} parameters in the SQL query
* @returns {Promise}
*/
run(sql, parameters) {
return new Promise((resolve, reject) => {
this.connection.run(sql, parameters, (error, result) => {
if (error)
reject(error);
resolve(result)
});
});
}
/**
* Run a SQL query against the database and retrieve all the rows.
* @param {String} sql SQL query
* @param {Array} parameters in the SQL query
* @returns {Promise}
*/
all(sql, parameters) {
return new Promise((resolve, reject) => {
this.connection.all(sql, parameters, (err, rows) => {
if (err) {
reject(err);
}
resolve(rows);
})
})
}
/**
* Run a SQL query against the database and retrieve one row.
* @param {String} sql SQL query
* @param {Array} parameters in the SQL query
* @returns {Promise}
*/
get(sql, parameters) {
return new Promise((resolve, reject) => {
this.connection.get(sql, parameters, (err, rows) => {
if (err) {
reject(err);
}
resolve(rows);
})
})
}
/**
* Run a SQL query against the database and retrieve the status.
* @param {String} sql SQL query
* @param {Array} parameters in the SQL query
* @returns {Promise}
*/
execute(sql) {
return new Promise(resolve => {
this.connection.exec(sql, (err, database) => {
if (err) {
console.log('ERROR: ', err);
reject(err);
}
resolve();
})
})
}
/**
* Setup the database by running setup.sql file in schemas/.
* @returns {Promise}
*/
setUp() {
const setupSchema = this.readSqlFile('setup.sql');
return Promise.resolve(this.execute(setupSchema));
}
/**
* Tears down the database by running tearDown.sql file in schemas/.
* @returns {Promise}
*/
tearDown() {
const tearDownSchema = this.readSqlFile('teardown.sql');
return Promise.resolve(this.execute(tearDownSchema));
}
/**
* Returns the file contents of a SQL file in schemas/.
* @returns {String}
*/
readSqlFile(filename) {
const schemaPath = path.join(this.schemaDirectory, filename);
const schema = fs.readFileSync(schemaPath).toString('utf-8');
return schema;
}
}
module.exports = SqliteDatabase;

View File

@@ -0,0 +1,9 @@
class GitRepository {
static dumpHook(body) {
/* eslint-disable no-console */
console.log(body);
}
}
module.exports = GitRepository;

View File

@@ -0,0 +1,19 @@
class Media {
constructor(title, year, type) {
this.title = title;
this.year = year;
this.type = type;
}
toString() {
return `N: ${this.title} | Y: ${this.year} | T: ${this.type}`;
}
print() {
/* eslint-disable no-console */
console.log(this.toString());
}
}
module.exports = Media;

View File

@@ -0,0 +1,15 @@
class MediaInfo {
constructor() {
this.duration = undefined;
this.height = undefined;
this.width = undefined;
this.bitrate = undefined;
this.resolution = undefined;
this.framerate = undefined;
this.protocol = undefined;
this.container = undefined;
this.audioCodec = undefined;
}
}
module.exports = MediaInfo;

View File

@@ -0,0 +1,12 @@
class Player {
constructor(device, address) {
this.device = device;
this.ip = address;
this.platform = undefined;
this.product = undefined;
this.title = undefined;
this.state = undefined;
}
}
module.exports = Player;

View File

@@ -0,0 +1,22 @@
const Media = require('src/media_classes/media');
class Plex extends Media {
constructor(title, year, type, summary, poster_path, background_path, added, seasons, episodes) {
super(title, year, type);
this.summary = summary;
this.poster_path = poster_path;
this.background_path = background_path;
this.added = added;
this.seasons = seasons;
this.episodes = episodes;
}
print() {
super.print();
}
}
module.exports = Plex;

View File

@@ -0,0 +1,33 @@
const Media = require('src/media_classes/media');
class TMDB extends Media {
// constructor(...args) {
constructor(title, year, type, id, summary, poster_path, background_path, popularity, score, release_status, tagline, seasons, episodes) {
super(title, year, type);
this.id = id;
this.summary = summary;
this.poster_path = poster_path;
this.background_path = background_path;
this.popularity = popularity;
this.score = score;
this.release_status = release_status;
this.tagline = tagline;
this.seasons = seasons;
this.episodes = episodes;
}
toString() {
return `${super.toString()} | ID: ${this.id}`;
}
print() {
/* eslint-disable no-console */
console.log(this.toString());
}
}
module.exports = TMDB;

View File

@@ -0,0 +1,8 @@
class User {
constructor(id, title) {
this.id = id;
this.title = title;
}
}
module.exports = User;

View File

@@ -0,0 +1,84 @@
const assert = require('assert');
const http = require('http');
const { URL } = require('url');
const PythonShell = require('python-shell');
const establishedDatabase = require('src/database/database');
function getMagnetFromURL(url) {
return new Promise((resolve, reject) => {
const options = new URL(url);
if (options.protocol.includes('magnet'))
resolve(url)
http.get(options, (res) => {
if (res.statusCode == 301 || res.statusCode == 302) {
resolve(res.headers.location)
}
});
});
}
async function find(searchterm, callback) {
const options = {
pythonPath: '../torrent_search/env/bin/python3',
scriptPath: '../torrent_search',
args: [searchterm, '-s', 'jackett', '-f', '--print']
}
PythonShell.run('torrentSearch/search.py', options, callback);
// PythonShell does not support return
}
async function callPythonAddMagnet(url, callback) {
getMagnetFromURL(url)
.then((magnet) => {
const options = {
pythonPath: '../delugeClient/env/bin/python3',
scriptPath: '../delugeClient',
args: ['add', magnet]
};
PythonShell.run('deluge_cli.py', options, callback);
})
.catch((err) => {
console.log(err);
throw new Error(err);
})
}
async function SearchPiratebay(query) {
return await new Promise((resolve, reject) => find(query, (err, results) => {
if (err) {
console.log('THERE WAS A FUCKING ERROR!\n', err);
reject(Error('There was a error when searching for torrents'));
}
if (results) {
resolve(JSON.parse(results, null, '\t'));
}
}));
}
async function AddMagnet(magnet, name, tmdb_id) {
return await new Promise((resolve, reject) => callPythonAddMagnet(magnet, (err, results) => {
if (err) {
/* eslint-disable no-console */
console.log(err);
reject(Error('Enable to add torrent', err))
}
/* eslint-disable no-console */
console.log('result/error:', err, results);
database = establishedDatabase;
insert_query = "INSERT INTO requested_torrent(magnet,torrent_name,tmdb_id) \
VALUES (?,?,?)";
let response = database.run(insert_query, [magnet, name, tmdb_id]);
console.log('Response from requsted_torrent insert: ' + response);
resolve({ success: true });
}));
}
module.exports = { SearchPiratebay, AddMagnet };

View File

@@ -0,0 +1,20 @@
const Episode = require('src/plex/types/episode');
function convertPlexToEpisode(plexEpisode) {
const episode = new Episode(plexEpisode.title, plexEpisode.grandparentTitle, plexEpisode.year);
episode.season = plexEpisode.parentIndex;
episode.episode = plexEpisode.index;
episode.summary = plexEpisode.summary;
episode.rating = plexEpisode.rating;
if (plexEpisode.viewCount !== undefined) {
episode.views = plexEpisode.viewCount;
}
if (plexEpisode.originallyAvailableAt !== undefined) {
episode.airdate = new Date(plexEpisode.originallyAvailableAt)
}
return episode;
}
module.exports = convertPlexToEpisode;

View File

@@ -0,0 +1,15 @@
const Movie = require('src/plex/types/movie');
function convertPlexToMovie(plexMovie) {
const movie = new Movie(plexMovie.title, plexMovie.year);
movie.rating = plexMovie.rating;
movie.tagline = plexMovie.tagline;
if (plexMovie.summary !== undefined) {
movie.summary = plexMovie.summary;
}
return movie;
}
module.exports = convertPlexToMovie;

View File

@@ -0,0 +1,24 @@
const Plex = require('src/media_classes/plex');
function translateAdded(date_string) {
return new Date(date_string * 1000);
}
function convertPlexToSeasoned(plex) {
const title = plex.title;
const year = plex.year;
const type = plex.type;
const summary = plex.summary;
const poster_path = plex.thumb;
const background_path = plex.art;
const added = translateAdded(plex.addedAt);
// const genre = plex.genre;
const seasons = plex.childCount;
const episodes = plex.leafCount;
const seasoned = new Plex(title, year, type, summary, poster_path, background_path, added, seasons, episodes);
// seasoned.print();
return seasoned;
}
module.exports = convertPlexToSeasoned;

View File

@@ -0,0 +1,13 @@
const Show = require('src/plex/types/show');
function convertPlexToShow(plexShow) {
const show = new Show(plexShow.title, plexShow.year);
show.summary = plexShow.summary;
show.rating = plexShow.rating;
show.seasons = plexShow.childCount;
show.episodes = plexShow.leafCount;
return show;
}
module.exports = convertPlexToShow;

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