137 Commits

Author SHA1 Message Date
1b7a754224 Merge pull request #131 from KevinMidboe/feat/credits
Feat/credits
2022-08-15 23:58:54 +02:00
5d91f1bae7 TODO sms code 2022-08-15 23:51:18 +02:00
cdcfae56e7 Allow set-cookie & allow credentials 2022-08-15 23:39:39 +02:00
f2c77e092d Safer imports in credits 2022-08-15 23:38:53 +02:00
d6ac7e55e9 Better matching when getting matchin plex title & year 2022-08-15 23:38:35 +02:00
a3543090f2 Translate plex query params to fixed 2022-08-15 23:37:07 +02:00
041e944783 Upgraded bcrypt & sqlite3 2022-08-15 23:36:02 +02:00
bfd31ebd23 Linting 2022-03-06 11:59:32 +01:00
5036f4ca36 Read and pass adult search query param consistently for movie, show & person info 2022-03-06 11:57:45 +01:00
61b59ae3ea Person info now handles optional credits query parameter 2022-03-06 11:55:32 +01:00
92c49ac523 Registered endpoint for person credits 2022-03-06 11:54:56 +01:00
f680642f25 When fetching credits for person we get movie & show objects, now handled here. 2022-03-06 11:54:22 +01:00
f89486ae9e Sepearate controller and tmdb function for fetching personCredits 2022-03-06 11:53:45 +01:00
4d853565d1 Person gets biography & place_of_birth attributes 2022-03-06 11:50:20 +01:00
91c81e5cf6 Use cookie-parser, updated tokenToUser middleware & set logout endpoint 2022-03-06 10:39:34 +01:00
0ecbde9675 Logout endpoint for deleting auth cookie 2022-03-06 10:36:24 +01:00
d8e951c2ef Can now pass settings to new Token. 2022-03-06 10:34:48 +01:00
90f3d86511 Removed unused Promise resolves 2022-03-06 10:34:39 +01:00
c6791a7027 Login and register builds and sets cookie auth token 2022-03-06 10:31:58 +01:00
5b6a2c2651 Linting 2022-03-06 10:27:09 +01:00
4f7a22fff1 New tokenToUser middleware checks both header and cookie for Auth token 2022-03-06 10:17:56 +01:00
31b0c998a8 Removed filter option when searching jackett 2022-01-03 19:28:50 +01:00
9ce5b476ef Prettierrc file 2022-01-03 19:28:42 +01:00
554f292e4c Don't fail request when plex failes existance check 2022-01-03 19:28:09 +01:00
d8985aaff7 Correctly use username from req.loggedInUser 2022-01-03 19:17:27 +01:00
be889b8100 Lining 2022-01-03 19:06:11 +01:00
b5bd672f44 More accurate checks for what response contains and throws error if it does not exist. 2022-01-03 18:38:32 +01:00
4501bc5302 Updated yarn lock file 2022-01-03 18:33:45 +01:00
b384e748af Updated and moved update requests to /scripts 2022-01-03 18:32:48 +01:00
c676f182b4 Cache time in HR & future cache bust function 2022-01-03 18:31:28 +01:00
95d2b0095b Linting 2022-01-03 18:23:49 +01:00
8165cf8e85 Keep whitespace when sanitizing string 2022-01-03 18:04:10 +01:00
14775744b0 Linting 2022-01-03 18:03:53 +01:00
559e32c059 Use list index 1 results 2022-01-03 18:02:58 +01:00
f4dbaf4c58 Translate symbol + to space in query 2022-01-03 18:02:45 +01:00
1d25914ae0 Linting 2022-01-03 17:56:00 +01:00
4d3d8c874c Moved cors allowed origins to conf file. 2020-04-08 23:17:45 +02:00
08433523b7 Correctly export poster_path from person tmdb response object. 2020-04-08 23:09:37 +02:00
fce8879994 Searching with adult parameter is now handled and also cached correctly. 2020-04-08 23:08:42 +02:00
505b126043 Cleanup, removed comments and logs. 2020-04-08 23:07:09 +02:00
589bd7b08d Better comparison between plex and tmdb. Now also checks if plexTitle starts with tmdbTitle. 2020-04-08 23:05:56 +02:00
f0049ffb4e Merge pull request #129 from KevinMidboe/feat/redis-cache
Feat/redis cache
2020-04-08 23:04:06 +02:00
2b25397253 Misc updates to caching from plex and tmdb. 2020-04-08 23:01:33 +02:00
776f83553a Changed more instances of tmdb to new TMDB constructor not needing cache instance anymore. 2020-04-08 22:59:23 +02:00
815aaedffb Changed tmdb constructor to have apiKey as first parameter. If cache is not defined we use redis. 2020-04-08 22:54:19 +02:00
578eff30fb Id of request has been incorrectly saved as string, this has dictated the returntype to the frontend. Now set from string to int. 2020-04-08 21:58:37 +02:00
943cbe5cb8 Return rejected promise and don't set cache if key or value is null. 2020-04-08 21:57:49 +02:00
f89db46bf2 findPlexItemByTitleAndYear returns a promise so we await its response. 2020-02-21 23:09:42 +01:00
085fb76e11 Cache pirate respons for 3 hours in redis. 2020-02-21 22:09:42 +01:00
aa4a1c2a57 Registed route watch-link, by title & year
Fetch watch link for plex item matching query parameters title and year.
2020-02-21 21:53:51 +01:00
74340afd16 Full re-write. Refactored, cache and watchlink.
- Now the api calls to plex are cached.
- Search function has been refactored.
- Exists in plex has been re-written to use findPlexItemByTitleAndYear
if anything is found we return true. findPlexItemByTitleAndYear is then
also used for our new endpoint to get direct watch link for plex item.
Function parameters are title and year. Title is used when querying plex
and year is used with title to match correct plex search result. The
direct link includes the machine identifier so a function was added to
get system information from plex (read: 'PLEX_URL/' e.g. base path).
2020-02-21 21:53:19 +01:00
2672266908 Simplified every function towards tmdb by having the shared functionality separated into a function. 2020-02-21 21:47:00 +01:00
f37786aa76 Redis cache script with get and set functions.
This replaces old way of caching to sqlite. Still have same
functionality, so users of old cache functions just need to rename their
import.
2020-02-21 18:11:39 +01:00
91f64e5cfb Redis new dependency 2020-02-21 18:09:04 +01:00
a4d3123910 Stricter check for plex search results. 2020-01-16 21:25:45 +01:00
bc6fe3ed48 Encode search query before searching plex. 2020-01-16 21:25:13 +01:00
b23566509f Merge pull request #128 from KevinMidboe/fix/match-list-plex
Fixed issue matching list of plex object to tmdb.
2020-01-16 21:22:34 +01:00
341a07621d Fixed issue matching list of plex object to tmdb.
We have a Plex function that allows us to input a tmdb object and a plex
search result too see if the tmdb object has anything similar when
searching in plex.
Fixed an issue where plex returned a list of items. This list is now
mapped over each list element.
2020-01-16 21:21:40 +01:00
259ed9b06f Error. Fixed capitalization of file name. 2019-12-25 21:12:29 +01:00
cddf06cbcc Travis should make a copy of the example development config. 2019-12-23 02:08:20 +01:00
318d1e331b Merge pull request #124 from KevinMidboe/feature/plex-authentication
Feature plex authentication
2019-12-23 01:39:43 +01:00
5923cbf051 Incorrect query parameter. Changed from (the not defined) plex_userid to username. 2019-12-22 13:49:16 +01:00
291bdf089c Forget to rename copied link function to unlink 2019-12-22 13:42:13 +01:00
8eacde9ccc Moved tautulli config settings to be fetched from our configuration (either env variable or conf/development.json. 2019-12-22 13:36:13 +01:00
f8847c62f2 UserRepository handles updating db settings better.
Moved the plex_userid to settings to expanded with functions for
updating and fetching settings, each with its own helper function
towards the database.
Since we had a linkPlexUserId function and we dont want plex_userid to
be updated from the updatesettings function we moved unlinking to a
separate endpoint and class function.
Also a new controller and endpoints for getting and updating settings.
2019-12-22 13:30:18 +01:00
ddb7e7379d Every instance of sqlite database should have foreign_keys constraints on 2019-12-22 13:14:12 +01:00
720fb69648 Indentation 2019-12-22 12:45:01 +01:00
fedacf498e Changed input parameter from name from user to username, cause we just get the username string. 2019-12-22 12:44:45 +01:00
9022853502 Sql schema now has requested_by as a foreign key for user_name and on delete set null clause. 2019-12-22 12:43:12 +01:00
c1b96e17ca Moved database row plex_userid from user to a new table settings. Currently includes plex_userid, emoji and darkmode with user_name as a foreign key to user.user_name. 2019-12-20 21:45:31 +01:00
a5248f0631 TODO comment for login 2019-11-25 23:38:52 +01:00
e2d85c6242 Merged into master branch 2019-11-24 19:26:56 +01:00
510c014549 Added endpoint for getting plays grouped by days of week. 2019-11-11 17:59:16 +01:00
2650497986 Tautulli will serve all the tautulli api calls. Mostly this will be used by user requests for getting stats related to the logged in user. WIP but currently watch time stats, plays by number of days, and view history has been implemented.
TODO:
 - Error handling if tautulli request fails.
 - Filter the responses and/or limit them from tautulli
 - Handle parsing variable parameters from request
2019-11-04 23:11:00 +01:00
639f0ec17a Created middleware to check if the user has a plex user linked to seasoned account. If not respond with 403 meaning you did have a authorization key, but this is forbidden; explaining in the response message that no plex account user id was found for this user and to please authenticate their plex account at authenticate endpoint. 2019-11-04 23:04:42 +01:00
977d05c6f2 Refactor. Responses should return error string in object key message not error. 2019-11-04 22:58:42 +01:00
601fc1d0de Renamed search_history controller to searchHistory 2019-11-04 22:57:59 +01:00
acc26a2f09 Renamed user history to search_history and fixed an issue where search history received the entire user object and not just the username 2019-11-04 20:32:41 +01:00
5d3a5dc8a4 Removed unused console log 2019-11-04 18:46:42 +01:00
3bb9bd84d9 Merge branch 'master' into feature/plex-authentication 2019-11-04 18:24:19 +01:00
ea5bc36956 Merge pull request #111 from KevinMidboe/api/v2
Api/v2
2019-11-04 18:01:15 +01:00
002e663be1 Merge branch 'api/v2' into feature/plex-authentication 2019-11-04 17:55:27 +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
495a3b4838 Merged api/v2 into feature branch 2019-11-04 00:58:43 +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
9e2a0101c9 Added new formdata pacakge 2019-11-04 00:44:57 +01:00
05b001de2e Created endpoint for authenticating with plex and linking your plex user to the logged in seasoned user. User database table gets a new plex_userid column. This will be used to fetch tautulli stats for your account. 2019-11-04 00:43:42 +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
1f9dc067e6 Plex params are now parsed with URI encoder. 2019-04-10 22:22:52 +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
99 changed files with 7960 additions and 2392 deletions

10
.prettierrc Normal file
View File

@@ -0,0 +1,10 @@
{
"tabWidth": 2,
"useTabs": false,
"semi": true,
"singleQuote": false,
"bracketSpacing": true,
"arrowParens": "avoid",
"vueIndentScriptAndStyle": false,
"trailingComma": "none"
}

View File

@@ -1,5 +1,5 @@
language: node_js language: node_js
node_js: '8.7.0' node_js: '11.9.0'
git: git:
submodules: true submodules: true
script: script:
@@ -7,6 +7,7 @@ script:
- yarn coverage - yarn coverage
before_install: before_install:
- cd seasoned_api - cd seasoned_api
- cp conf/development.json.example conf/development.json
before_script: before_script:
- yarn - yarn
- curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter

View File

@@ -10,8 +10,8 @@
<img src="https://travis-ci.org/KevinMidboe/seasonedShows.svg?branch=master" <img src="https://travis-ci.org/KevinMidboe/seasonedShows.svg?branch=master"
alt="Travis CI"> alt="Travis CI">
</a> </a>
<a href="https://coveralls.io/github/KevinMidboe/seasonedShows?branch=coverage"> <a href="https://coveralls.io/github/KevinMidboe/seasonedShows?branch=api/v2">
<img src="https://coveralls.io/repos/github/KevinMidboe/seasonedShows/badge.svg?branch=coverage" alt=""> <img src="https://coveralls.io/repos/github/KevinMidboe/seasonedShows/badge.svg?branch=api/v2" alt="">
</a> </a>
<a href="https://snyk.io/test/github/KevinMidboe/seasonedShows?targetFile=seasoned_api/package.json"> <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=""> <img src="https://snyk.io/test/github/KevinMidboe/seasonedShows/badge.svg?targetFile=seasoned_api/package.json" alt="">

View File

@@ -12,7 +12,7 @@
}, },
"dependencies": { "dependencies": {
"clean-webpack-plugin": "^0.1.17", "clean-webpack-plugin": "^0.1.17",
"css-loader": "^0.28.4", "css-loader": "^1.0.0",
"html-webpack-plugin": "^2.28.0", "html-webpack-plugin": "^2.28.0",
"path": "^0.12.7", "path": "^0.12.7",
"react": "^15.6.1", "react": "^15.6.1",
@@ -30,8 +30,8 @@
"redux-thunk": "^2.2.0", "redux-thunk": "^2.2.0",
"urijs": "^1.18.12", "urijs": "^1.18.12",
"webfontloader": "^1.6.28", "webfontloader": "^1.6.28",
"webpack": "^3.5.5", "webpack": "^4.0.0",
"webpack-dev-server": "^2.4.5", "webpack-dev-server": "^3.1.11",
"webpack-merge": "^4.1.0" "webpack-merge": "^4.1.0"
}, },
"devDependencies": { "devDependencies": {

View File

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

View File

@@ -7,39 +7,51 @@
}, },
"main": "webserver/server.js", "main": "webserver/server.js",
"scripts": { "scripts": {
"start": "cross-env SEASONED_CONFIG=conf/development.json PROD=true NODE_PATH=. node src/webserver/server.js", "start": "cross-env SEASONED_CONFIG=conf/development.json NODE_ENV=production NODE_PATH=. babel-node src/webserver/server.js",
"test": "cross-env SEASONED_CONFIG=conf/test.json NODE_PATH=. mocha --recursive test/unit test/system", "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 --recursive test && nyc report --reporter=text-lcov | coveralls", "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/", "lint": "./node_modules/.bin/eslint src/",
"update": "cross-env SEASONED_CONFIG=conf/development.json NODE_PATH=. node src/plex/updateRequestsInPlex.js" "update": "cross-env SEASONED_CONFIG=conf/development.json NODE_PATH=. node scripts/updateRequestsInPlex.js",
"docs": "yarn apiDocs; yarn classDocs",
"apiDocs": "",
"classDocs": "./script/generate-class-docs.sh"
}, },
"dependencies": { "dependencies": {
"axios": "^0.18.0", "axios": "^0.18.0",
"bcrypt-nodejs": "^0.0.3", "bcrypt": "^5.0.1",
"body-parser": "~1.18.2", "body-parser": "~1.18.2",
"cookie-parser": "^1.4.6",
"cross-env": "~5.1.4", "cross-env": "~5.1.4",
"express": "~4.16.0", "express": "~4.16.0",
"jsonwebtoken": "^8.0.1", "form-data": "^2.5.1",
"km-moviedb": "^0.2.13", "jsonwebtoken": "^8.5.1",
"mongoose": "~5.0.11",
"km-moviedb": "^0.2.12", "km-moviedb": "^0.2.12",
"node-cache": "^4.1.1", "node-cache": "^4.1.1",
"node-fetch": "^2.6.0",
"python-shell": "^0.5.0", "python-shell": "^0.5.0",
"request": "^2.85.0", "raven": "^2.4.2",
"redis": "^3.0.2",
"request": "^2.87.0",
"request-promise": "^4.2", "request-promise": "^4.2",
"sqlite3": "^4.0.0" "sqlite3": "^5.0.1"
}, },
"devDependencies": { "devDependencies": {
"coveralls": "^3.0.0", "@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": "^4.9.0",
"eslint-config-airbnb-base": "^12.1.0", "eslint-config-airbnb-base": "^12.1.0",
"eslint-plugin-import": "^2.8.0", "eslint-plugin-import": "^2.8.0",
"istanbul": "^0.4.5", "istanbul": "^0.4.5",
"mocha": "^5.0.4", "mocha": "^6.2.0",
"mocha-lcov-reporter": "^1.3.0", "mocha-lcov-reporter": "^1.3.0",
"nyc": "^11.6.0", "nyc": "^11.6.0",
"raven": "^2.4.2",
"supertest": "^3.0.0", "supertest": "^3.0.0",
"supertest-as-promised": "^4.0.1" "supertest-as-promised": "^4.0.1",
"typescript": "^3.5.3"
} }
} }

View File

@@ -0,0 +1,44 @@
const Plex = require("src/plex/plex");
const configuration = require("src/config/configuration").getInstance();
const plex = new Plex(configuration.get("plex", "ip"));
const establishedDatabase = require("src/database/database");
const queries = {
getRequestsNotYetInPlex: `SELECT * FROM requests WHERE status = 'requested' OR status = 'downloading'`,
saveNewStatus: `UPDATE requests SET status = ? WHERE id IS ? and type IS ?`
};
const getByStatus = () =>
establishedDatabase.all(queries.getRequestsNotYetInPlex);
const checkIfRequestExistInPlex = async request => {
request.existsInPlex = await plex.existsInPlex(request);
return request;
};
const commitNewStatus = (status, id, type, title) => {
console.log(type, title, "updated to:", status);
return establishedDatabase.run(queries.saveNewStatus, [status, id, type]);
};
const getNewRequestMatchesInPlex = async () => {
const requests = await getByStatus();
return Promise.all(requests.map(checkIfRequestExistInPlex))
.catch(error =>
console.log("error from checking plex for existance:", error)
)
.then(matchedRequests =>
matchedRequests.filter(request => request.existsInPlex)
);
};
const updateMatchInDb = (match, status) => {
return commitNewStatus(status, match.id, match.type, match.title);
};
getNewRequestMatchesInPlex()
.then(newMatches =>
Promise.all(newMatches.map(match => updateMatchInDb(match, "downloaded")))
)
.then(() => process.exit(0));

52
seasoned_api/src/cache/redis.js vendored Normal file
View File

@@ -0,0 +1,52 @@
const redis = require("redis")
const client = redis.createClient()
class Cache {
/**
* Retrieve an unexpired cache entry by key.
* @param {String} key of the cache entry
* @returns {Promise}
*/
get(key) {
return new Promise((resolve, reject) => {
client.get(key, (error, reply) => {
if (reply == null) {
return reject();
}
resolve(JSON.parse(reply));
});
});
}
/**
* Insert cache entry with key and value.
* @param {String} key of the cache entry
* @param {String} value of the cache entry
* @param {Number} timeToLive the number of seconds before entry expires
* @returns {Object}
*/
set(key, value, timeToLive = 10800) {
if (value == null || key == null) return null;
const json = JSON.stringify(value);
client.set(key, json, (error, reply) => {
if (reply == "OK") {
// successfully set value with key, now set TTL for key
client.expire(key, timeToLive, e => {
if (e)
console.error(
"Unexpected error while setting expiration for key:",
key,
". Error:",
error
);
});
}
});
return value;
}
}
module.exports = Cache;

View File

@@ -22,13 +22,13 @@ class Config {
get(section, option) { get(section, option) {
if (this.fields[section] === undefined || this.fields[section][option] === undefined) { if (this.fields[section] === undefined || this.fields[section][option] === undefined) {
throw new Error(`Filed "${section} => ${option}" does not exist.`); throw new Error(`Field "${section} => ${option}" does not exist.`);
} }
const field = new Field(this.fields[section][option]); const field = new Field(this.fields[section][option]);
if (field.value === '') { if (field.value === '') {
const envField = process.env[['SEASONED', section.toUpperCase(), option.toUpperCase()].join('_')]; const envField = process.env[[section.toUpperCase(), option.toUpperCase()].join('_')];
if (envField !== undefined && envField.length !== 0) { return envField; } if (envField !== undefined && envField.length !== 0) { return envField; }
} }

View File

@@ -1,11 +1,19 @@
CREATE TABLE IF NOT EXISTS user ( CREATE TABLE IF NOT EXISTS user (
user_name varchar(127) UNIQUE, user_name varchar(127) UNIQUE,
password varchar(127), password varchar(127),
email varchar(127) UNIQUE,
admin boolean DEFAULT 0, admin boolean DEFAULT 0,
email varchar(127) UNIQUE,
primary key (user_name) primary key (user_name)
); );
CREATE TABLE IF NOT EXISTS settings (
user_name varchar(127) UNIQUE,
dark_mode boolean DEFAULT 0,
plex_userid varchar(127) DEFAULT NULL,
emoji varchar(16) DEFAULT NULL,
foreign key(user_name) REFERENCES user(user_name) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS cache ( CREATE TABLE IF NOT EXISTS cache (
key varchar(255), key varchar(255),
value blob, value blob,
@@ -23,17 +31,18 @@ CREATE TABLE IF NOT EXISTS search_history (
); );
CREATE TABLE IF NOT EXISTS requests( CREATE TABLE IF NOT EXISTS requests(
id TEXT, id NUMBER,
title TEXT, title TEXT,
year NUMBER, year NUMBER,
poster_path TEXT DEFAULT NULL, poster_path TEXT DEFAULT NULL,
background_path TEXT DEFAULT NULL, background_path TEXT DEFAULT NULL,
requested_by TEXT, requested_by varchar(127) DEFAULT NULL,
ip TEXT, ip TEXT,
date DATE DEFAULT CURRENT_TIMESTAMP, date DATE DEFAULT CURRENT_TIMESTAMP,
status CHAR(25) DEFAULT 'requested' NOT NULL, status CHAR(25) DEFAULT 'requested' NOT NULL,
user_agent CHAR(255) DEFAULT NULL, user_agent CHAR(255) DEFAULT NULL,
type CHAR(50) DEFAULT 'movie' type CHAR(50) DEFAULT 'movie',
foreign key(requested_by) REFERENCES user(user_name) ON DELETE SET NULL
); );
CREATE TABLE IF NOT EXISTS request( CREATE TABLE IF NOT EXISTS request(

View File

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

View File

@@ -6,6 +6,7 @@ class SqliteDatabase {
constructor(host) { constructor(host) {
this.host = host; this.host = host;
this.connection = new sqlite3.Database(this.host); this.connection = new sqlite3.Database(this.host);
this.execute('pragma foreign_keys = on;');
this.schemaDirectory = path.join(__dirname, 'schemas'); this.schemaDirectory = path.join(__dirname, 'schemas');
} }
@@ -25,7 +26,7 @@ class SqliteDatabase {
* @param {Array} parameters in the SQL query * @param {Array} parameters in the SQL query
* @returns {Promise} * @returns {Promise}
*/ */
async run(sql, parameters) { run(sql, parameters) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.connection.run(sql, parameters, (error, result) => { this.connection.run(sql, parameters, (error, result) => {
if (error) if (error)
@@ -41,7 +42,7 @@ class SqliteDatabase {
* @param {Array} parameters in the SQL query * @param {Array} parameters in the SQL query
* @returns {Promise} * @returns {Promise}
*/ */
async all(sql, parameters) { all(sql, parameters) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.connection.all(sql, parameters, (err, rows) => { this.connection.all(sql, parameters, (err, rows) => {
if (err) { if (err) {
@@ -58,7 +59,7 @@ class SqliteDatabase {
* @param {Array} parameters in the SQL query * @param {Array} parameters in the SQL query
* @returns {Promise} * @returns {Promise}
*/ */
async get(sql, parameters) { get(sql, parameters) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.connection.get(sql, parameters, (err, rows) => { this.connection.get(sql, parameters, (err, rows) => {
if (err) { if (err) {
@@ -75,7 +76,7 @@ class SqliteDatabase {
* @param {Array} parameters in the SQL query * @param {Array} parameters in the SQL query
* @returns {Promise} * @returns {Promise}
*/ */
async execute(sql) { execute(sql) {
return new Promise(resolve => { return new Promise(resolve => {
this.connection.exec(sql, (err, database) => { this.connection.exec(sql, (err, database) => {
if (err) { if (err) {

View File

@@ -0,0 +1,35 @@
const request = require("request");
const configuration = require('src/config/configuration').getInstance();
const sendSMS = (message) => {
const apiKey = configuration.get('sms', 'apikey')
if (!apiKey) {
console.warning("api key for sms not set, cannot send sms.")
return null
}
const sender = configuration.get('sms', 'sender')
const recipient = configuration.get('sms', 'recipient')
return new Promise((resolve, reject) => {
request.post(
{
url: `https://gatewayapi.com/rest/mtsms?token=${apiKey}`,
json: true,
body: {
sender,
message,
recipients: [{ msisdn: `47${recipient}` }]
}
},
function(err, r, body) {
console.log(err ? err : body);
console.log("sms provider response:", body)
resolve()
}
);
})
}
module.exports = { sendSMS }

View File

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

View File

@@ -1,59 +1,240 @@
const axios = require('axios') const fetch = require("node-fetch");
const convertPlexToMovie = require('src/plex/convertPlexToMovie') const convertPlexToMovie = require("src/plex/convertPlexToMovie");
const convertPlexToShow = require('src/plex/convertPlexToShow') const convertPlexToShow = require("src/plex/convertPlexToShow");
const convertPlexToEpisode = require('src/plex/convertPlexToEpisode') const convertPlexToEpisode = require("src/plex/convertPlexToEpisode");
class Plex { const { Movie, Show, Person } = require("src/tmdb/types");
constructor(ip) {
this.plexIP = ip const RedisCache = require("src/cache/redis");
this.plexPort = 32400 const redisCache = new RedisCache();
const sanitize = string => string.toLowerCase().replace(/[^\w]/gi, "");
function fixedEncodeURIComponent(str) {
return encodeURIComponent(str).replace(/[!'()*]/g, function (c) {
return "%" + c.charCodeAt(0).toString(16).toUpperCase();
});
}
const matchingTitleAndYear = (plex, tmdb) => {
let matchingTitle, matchingYear;
if (plex["title"] != null && tmdb["title"] != null) {
const plexTitle = sanitize(plex.title);
const tmdbTitle = sanitize(tmdb.title);
matchingTitle = plexTitle == tmdbTitle;
matchingTitle = matchingTitle
? matchingTitle
: plexTitle.startsWith(tmdbTitle);
} else matchingTitle = false;
if (plex["year"] != null && tmdb["year"] != null)
matchingYear = plex.year == tmdb.year;
else matchingYear = false;
return matchingTitle && matchingYear;
};
const successfullResponse = response => {
if (response && response["MediaContainer"]) return response;
if (
response == null ||
response["status"] == null ||
response["statusText"] == null
) {
throw Error("Unable to decode response");
} }
existsInPlex(tmdbMovie) { const { status, statusText } = response;
return Promise.resolve()
.then(() => this.search(tmdbMovie.title))
// TODO handle this when whitelist of local ip is not set in plex
.catch((error) => { console.error('Unable to search plex')})
.then((plexMovies) => {
const matches = plexMovies.some((plexMovie) => {
return tmdbMovie.title === plexMovie.title && tmdbMovie.type === plexMovie.type
})
tmdbMovie.existsInPlex = matches if (status === 200) {
return tmdbMovie return response.json();
}) } else {
throw { message: statusText, status: status };
}
};
class Plex {
constructor(ip, port = 32400, cache = null) {
this.plexIP = ip;
this.plexPort = port;
this.cache = cache || redisCache;
this.cacheTags = {
machineInfo: "plex/mi",
search: "plex/s"
};
}
fetchMachineIdentifier() {
const cacheKey = `${this.cacheTags.machineInfo}`;
const url = `http://${this.plexIP}:${this.plexPort}/`;
const options = {
timeout: 20000,
headers: { Accept: "application/json" }
};
return new Promise((resolve, reject) =>
this.cache
.get(cacheKey)
.then(machineInfo => resolve(machineInfo["machineIdentifier"]))
.catch(() => fetch(url, options))
.then(response => response.json())
.then(machineInfo =>
this.cache.set(cacheKey, machineInfo["MediaContainer"], 2628000)
)
.then(machineInfo => resolve(machineInfo["machineIdentifier"]))
.catch(error => {
if (error != undefined && error.type === "request-timeout") {
reject({
message: "Plex did not respond",
status: 408,
success: false
});
}
reject(error);
})
);
}
matchTmdbAndPlexMedia(plex, tmdb) {
let match;
if (plex == null || tmdb == null) return false;
if (plex instanceof Array) {
let possibleMatches = plex.map(plexItem =>
matchingTitleAndYear(plexItem, tmdb)
);
match = possibleMatches.includes(true);
} else {
match = matchingTitleAndYear(plex, tmdb);
}
return match;
}
async existsInPlex(tmdb) {
const plexMatch = await this.findPlexItemByTitleAndYear(
tmdb.title,
tmdb.year
);
return plexMatch ? true : false;
}
findPlexItemByTitleAndYear(title, year) {
const query = { title, year };
return this.search(title).then(plexResults => {
const matchesInPlex = plexResults.map(plex =>
this.matchTmdbAndPlexMedia(plex, query)
);
const matchesIndex = matchesInPlex.findIndex(el => el === true);
return matchesInPlex != -1 ? plexResults[matchesIndex] : null;
});
}
getDirectLinkByTitleAndYear(title, year) {
const machineIdentifierPromise = this.fetchMachineIdentifier();
const matchingObjectInPlexPromise = this.findPlexItemByTitleAndYear(
title,
year
);
return Promise.all([
machineIdentifierPromise,
matchingObjectInPlexPromise
]).then(([machineIdentifier, matchingObjectInPlex]) => {
if (
matchingObjectInPlex == false ||
matchingObjectInPlex == null ||
matchingObjectInPlex["key"] == null ||
machineIdentifier == null
)
return false;
const keyUriComponent = fixedEncodeURIComponent(matchingObjectInPlex.key);
return `https://app.plex.tv/desktop#!/server/${machineIdentifier}/details?key=${keyUriComponent}`;
});
} }
search(query) { search(query) {
const options = { const cacheKey = `${this.cacheTags.search}:${query}`;
baseURL: `http://${this.plexIP}:${this.plexPort}`,
url: '/hubs/search',
params: { query: query },
responseType: 'json',
timeout: 3000
}
return Promise.resolve() const url = `http://${this.plexIP}:${
.then(() => axios.request(options)) this.plexPort
.catch((error) => { throw new Error(`Unable to search plex library`, error) }) }/hubs/search?query=${fixedEncodeURIComponent(query)}`;
.then(response => this.mapResults(response)) const options = {
timeout: 20000,
headers: { Accept: "application/json" }
};
return new Promise((resolve, reject) =>
this.cache
.get(cacheKey)
.catch(() => fetch(url, options)) // else fetch fresh data
.then(successfullResponse)
.then(results => this.cache.set(cacheKey, results, 21600)) // 6 hours
.then(this.mapResults)
.then(resolve)
.catch(error => {
if (error != undefined && error.type === "request-timeout") {
reject({
message: "Plex did not respond",
status: 408,
success: false
});
}
reject(error);
})
);
} }
// this is not guarenteed to work, but if we see a movie or
// show has been imported, this function can be helpfull to call
// in order to try bust the cache preventing movieInfo and
// showInfo from seeing updates through existsInPlex.
bustSearchCacheWithTitle(title) {
const query = title;
const cacheKey = `${this.cacheTags.search}/${query}*`;
this.cache.del(
cacheKey,
(error,
response => {
if (response == 1) return true;
// TODO improve cache key matching by lowercasing it on the backend.
// what do we actually need to check for if the key was deleted or not
// it might be an error or another response code.
console.log("Unable to delete, key might not exists");
})
);
}
mapResults(response) { mapResults(response) {
return response.data.MediaContainer.Hub.reduce((result, hub) => { if (
if (hub.type === 'movie' && hub.Metadata !== undefined) { response == null ||
return [...result, ...hub.Metadata.map(convertPlexToMovie)] response.MediaContainer == null ||
} response.MediaContainer.Hub == null
else if (hub.type === 'show' && hub.Metadata !== undefined) { ) {
return [...result, ...hub.Metadata.map(convertPlexToShow)] return [];
} }
else if (hub.type === 'episode' && hub.Metadata !== undefined) {
return [...result, ...hub.Metadata.map(convertPlexToEpisode)]
}
return result return response.MediaContainer.Hub.filter(category => category.size > 0)
}, []) .map(category => {
if (category.type === "movie") {
return category.Metadata;
} else if (category.type === "show") {
return category.Metadata.map(convertPlexToShow);
} else if (category.type === "episode") {
return category.Metadata.map(convertPlexToEpisode);
}
})
.filter(result => result !== undefined);
} }
} }

View File

@@ -1,101 +1,130 @@
const PlexRepository = require('src/plex/plexRepository'); const PlexRepository = require("src/plex/plexRepository");
const Cache = require('src/tmdb/cache'); const configuration = require("src/config/configuration").getInstance();
const configuration = require('src/config/configuration').getInstance(); const TMDB = require("src/tmdb/tmdb");
const TMDB = require('src/tmdb/tmdb'); const establishedDatabase = require("src/database/database");
const establishedDatabase = require('src/database/database');
const plexRepository = new PlexRepository(configuration.get('plex', 'ip')); const plexRepository = new PlexRepository(configuration.get("plex", "ip"));
const cache = new Cache(); const tmdb = new TMDB(configuration.get("tmdb", "apiKey"));
const tmdb = new TMDB(cache, configuration.get('tmdb', 'apiKey'));
class RequestRepository { class RequestRepository {
constructor(database) { constructor(database) {
this.database = database || establishedDatabase; this.database = database || establishedDatabase;
this.queries = { this.queries = {
insertRequest: `INSERT INTO requests(id,title,year,poster_path,background_path,requested_by,ip,user_agent,type) insertRequest: `INSERT INTO requests(id,title,year,poster_path,background_path,requested_by,ip,user_agent,type)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
fetchRequestedItems: 'SELECT * FROM requests ORDER BY date DESC LIMIT 25 OFFSET ?*25-25', fetchRequestedItems:
fetchRequestedItemsByStatus: 'SELECT * FROM requests WHERE status IS ? AND type LIKE ? ORDER BY date DESC LIMIT 25 OFFSET ?*25-25', "SELECT * FROM requests ORDER BY date DESC LIMIT 25 OFFSET ?*25-25",
updateRequestedById: 'UPDATE requests SET status = ? WHERE id is ? AND type is ?', fetchRequestedItemsByStatus:
checkIfIdRequested: 'SELECT * FROM requests WHERE id IS ? AND type IS ?', "SELECT * FROM requests WHERE status IS ? AND type LIKE ? ORDER BY date DESC LIMIT 25 OFFSET ?*25-25",
userRequests: 'SELECT * FROM requests WHERE requested_by IS ? ORDER BY date DESC', updateRequestedById:
}; "UPDATE requests SET status = ? WHERE id is ? AND type is ?",
this.cacheTags = { checkIfIdRequested: "SELECT * FROM requests WHERE id IS ? AND type IS ?",
search: 'se', userRequests:
lookup: 'i', "SELECT * FROM requests WHERE requested_by IS ? ORDER BY date DESC"
}; };
} this.cacheTags = {
search: "se",
lookup: "i"
};
}
search(query, type, page) { search(query, type, page) {
return Promise.resolve() return Promise.resolve()
.then(() => tmdb.search(query, type, page)) .then(() => tmdb.search(query, type, page))
.catch(error => Error(`error in the house${error}`)); .catch(error => Error(`error in the house${error}`));
} }
lookup(identifier, type = 'movie') { lookup(identifier, type = "movie") {
return Promise.resolve() return Promise.resolve()
.then(() => tmdb.lookup(identifier, type)) .then(() => tmdb.lookup(identifier, type))
.then(tmdbMovie => this.checkID(tmdbMovie)) .then(tmdbMovie => this.checkID(tmdbMovie))
.then(tmdbMovie => plexRepository.inPlex(tmdbMovie)) .then(tmdbMovie => plexRepository.inPlex(tmdbMovie))
.catch((error) => { .catch(error => {
throw new Error(error); throw new Error(error);
}); });
} }
checkID(tmdbMovie) { checkID(tmdbMovie) {
return Promise.resolve() return Promise.resolve()
.then(() => this.database.get(this.queries.checkIfIdRequested, [tmdbMovie.id, tmdbMovie.type])) .then(() =>
.then((result, error) => { this.database.get(this.queries.checkIfIdRequested, [
if (error) { throw new Error(error); } tmdbMovie.id,
tmdbMovie.requested = result ? true : false; tmdbMovie.type
return tmdbMovie; ])
}); )
} .then((result, error) => {
if (error) {
throw new Error(error);
}
tmdbMovie.requested = result ? true : false;
return tmdbMovie;
});
}
/** /**
* Send request for given media id. * Send request for given media id.
* @param {identifier, type} the id of the media object and type of media must be defined * @param {identifier, type} the id of the media object and type of media must be defined
* @returns {Promise} If nothing has gone wrong. * @returns {Promise} If nothing has gone wrong.
*/ */
sendRequest(identifier, type, ip, user_agent, user) { sendRequest(identifier, type, ip, user_agent, user) {
return Promise.resolve() return Promise.resolve()
.then(() => tmdb.lookup(identifier, type)) .then(() => tmdb.lookup(identifier, type))
.then((movie) => { .then(movie => {
const username = user === undefined ? undefined : user.username; const username = user === undefined ? undefined : user.username;
// Add request to database // Add request to database
return this.database.run(this.queries.insertRequest, [movie.id, movie.title, movie.year, movie.poster_path, movie.background_path, username, ip, user_agent, movie.type]); return this.database.run(this.queries.insertRequest, [
movie.id,
movie.title,
movie.year,
movie.poster_path,
movie.background_path,
username,
ip,
user_agent,
movie.type
]);
}); });
} }
fetchRequested(status, page = '1', type = '%') { fetchRequested(status, page = "1", type = "%") {
return Promise.resolve() return Promise.resolve().then(() => {
.then(() => { if (
if (status === 'requested' || status === 'downloading' || status === 'downloaded') status === "requested" ||
return this.database.all(this.queries.fetchRequestedItemsByStatus, [status, type, page]); status === "downloading" ||
else status === "downloaded"
return this.database.all(this.queries.fetchRequestedItems, page); )
}) return this.database.all(this.queries.fetchRequestedItemsByStatus, [
} status,
type,
page
]);
else return this.database.all(this.queries.fetchRequestedItems, page);
});
}
userRequests(user) { userRequests(username) {
return Promise.resolve() return Promise.resolve()
.then(() => this.database.all(this.queries.userRequests, user.username)) .then(() => this.database.all(this.queries.userRequests, username))
.catch((error) => { .catch(error => {
if (String(error).includes('no such column')) { if (String(error).includes("no such column")) {
throw new Error('Username not found'); throw new Error("Username not found");
} }
throw new Error('Unable to fetch your requests'); throw new Error("Unable to fetch your requests");
}) })
.then((result) => { .then(result => {
// TODO do a correct mapping before sending, not just a dump of the database // TODO do a correct mapping before sending, not just a dump of the database
result.map(item => item.poster = item.poster_path) result.map(item => (item.poster = item.poster_path));
return result return result;
}); });
} }
updateRequestedById(id, type, status) { updateRequestedById(id, type, status) {
return this.database.run(this.queries.updateRequestedById, [status, id, type]); return this.database.run(this.queries.updateRequestedById, [
} status,
id,
type
]);
}
} }
module.exports = RequestRepository; module.exports = RequestRepository;

View File

@@ -6,7 +6,6 @@ class Show {
this.rating = null; this.rating = null;
this.seasons = null; this.seasons = null;
this.episodes = null; this.episodes = null;
this.type = 'show';
} }
} }

View File

@@ -1,9 +1,7 @@
const assert = require('assert') const assert = require('assert')
const configuration = require('src/config/configuration').getInstance(); const configuration = require('src/config/configuration').getInstance();
const Cache = require('src/tmdb/cache');
const TMDB = require('src/tmdb/tmdb'); const TMDB = require('src/tmdb/tmdb');
const cache = new Cache(); const tmdb = new TMDB(configuration.get('tmdb', 'apiKey'));
const tmdb = new TMDB(cache, configuration.get('tmdb', 'apiKey'));
const establishedDatabase = require('src/database/database'); const establishedDatabase = require('src/database/database');
const utils = require('./utils'); const utils = require('./utils');
@@ -23,6 +21,7 @@ class RequestRepository {
downloaded: '(select status from requests where id is request.id and type is request.type limit 1)', downloaded: '(select status from requests where id is request.id and type is request.type limit 1)',
// deluge: '(select status from deluge_torrent where id is request.id and type is request.type limit 1)', // deluge: '(select status from deluge_torrent where id is request.id and type is request.type limit 1)',
// fetchAllFilterStatus: 'select * from request where ' // fetchAllFilterStatus: 'select * from request where '
readWithoutUserData: 'select id, title, year, type, status, date from requests where id is ? and type is ?',
read: 'select id, title, year, type, status, requested_by, ip, date, user_agent from requests where id is ? and type is ?' read: 'select id, title, year, type, status, requested_by, ip, date, user_agent from requests where id is ? and type is ?'
}; };
} }
@@ -85,18 +84,18 @@ class RequestRepository {
* @param {tmdb} tmdb class of movie|show to add * @param {tmdb} tmdb class of movie|show to add
* @returns {Promise} * @returns {Promise}
*/ */
requestFromTmdb(tmdb, ip, user_agent, user) { requestFromTmdb(tmdb, ip, user_agent, username) {
return Promise.resolve() return Promise.resolve()
.then(() => this.database.get(this.queries.read, [tmdb.id, tmdb.type])) .then(() => this.database.get(this.queries.read, [tmdb.id, tmdb.type]))
.then(row => assert.equal(row, undefined, 'Id has already been requested')) .then(row => assert.equal(row, undefined, 'Id has already been requested'))
.then(() => this.database.run(this.queries.add, [tmdb.id, tmdb.title, tmdb.year, tmdb.poster, tmdb.backdrop, user, ip, user_agent, tmdb.type])) .then(() => this.database.run(this.queries.add, [tmdb.id, tmdb.title, tmdb.year, tmdb.poster, tmdb.backdrop, username, ip, user_agent, tmdb.type]))
.catch((error) => { .catch((error) => {
if (error.name === 'AssertionError' || error.message.endsWith('been requested')) { if (error.name === 'AssertionError' || error.message.endsWith('been requested')) {
throw new Error('This id is already requested', error.message); throw new Error('This id is already requested', error.message);
} }
console.log('Error @ request.addTmdb:', error); console.log('Error @ request.addTmdb:', error);
throw new Error('Could not add request'); throw new Error('Could not add request');
}); });
} }
/** /**
@@ -106,11 +105,17 @@ class RequestRepository {
* @returns {Promise} * @returns {Promise}
*/ */
getRequestByIdAndType(id, type) { getRequestByIdAndType(id, type) {
return Promise.resolve() return this.database.get(this.queries.readWithoutUserData, [id, type])
.then(() => this.database.get(this.queries.read, [id, type]))
.then(row => { .then(row => {
assert(row, 'Could not find request item with that id and type') assert(row, 'Could not find request item with that id and type')
return JSON.stringify(row) return {
id: row.id,
title: row.title,
year: row.year,
type: row.type,
status: row.status,
requested_date: new Date(row.date)
}
}) })
} }
@@ -124,6 +129,7 @@ class RequestRepository {
*/ */
fetchAll(page=1, sort_by=undefined, sort_direction='asc', filter=undefined, query=undefined) { fetchAll(page=1, sort_by=undefined, sort_direction='asc', filter=undefined, query=undefined) {
// TODO implemented sort and filter // TODO implemented sort and filter
page = parseInt(page)
let fetchQuery = this.queries.fetchAll let fetchQuery = this.queries.fetchAll
let fetchTotalResults = this.queries.totalRequests let fetchTotalResults = this.queries.totalRequests
let fetchParams = [page] let fetchParams = [page]
@@ -144,11 +150,15 @@ class RequestRepository {
const totalRequests = sqliteResponse['totalRequests'] const totalRequests = sqliteResponse['totalRequests']
const totalPages = Math.ceil(totalRequests / 26) const totalPages = Math.ceil(totalRequests / 26)
return [ rows.map(item => { item.poster = item.poster_path; return item }), totalPages ] return [ rows.map(item => {
item.poster = item.poster_path; delete item.poster_path;
item.backdrop = item.background_path; delete item.background_path;
return item
}), totalPages, totalRequests ]
return Promise.all(this.mapToTmdbByType(rows)) return Promise.all(this.mapToTmdbByType(rows))
}) })
.then(([result, totalPages]) => Promise.resolve({ .then(([result, totalPages, totalRequests]) => Promise.resolve({
results: result, total_results: result.length, page: page, total_pages: totalPages results: result, total_results: totalRequests, page: page, total_pages: totalPages
})) }))
.catch(error => { console.log(error);throw error }) .catch(error => { console.log(error);throw error })
} }

View File

@@ -28,17 +28,23 @@ class SearchHistory {
/** /**
* Creates a new search entry in the database. * Creates a new search entry in the database.
* @param {User} user a new user * @param {String} username logged in user doing the search
* @param {String} searchQuery the query the user searched for * @param {String} searchQuery the query the user searched for
* @returns {Promise} * @returns {Promise}
*/ */
create(user, searchQuery) { create(username, searchQuery) {
return Promise.resolve() return this.database.run(this.queries.create, [searchQuery, username])
.then(() => this.database.run(this.queries.create, [searchQuery, user])) .catch(error => {
.catch((error) => {
if (error.message.includes('FOREIGN')) { if (error.message.includes('FOREIGN')) {
throw new Error('Could not create search history.'); throw new Error('Could not create search history.');
} }
throw {
success: false,
status: 500,
message: 'An unexpected error occured',
source: 'database'
}
}); });
} }
} }

View File

@@ -0,0 +1,74 @@
const fetch = require("node-fetch");
class Tautulli {
constructor(apiKey, ip, port) {
this.apiKey = apiKey;
this.ip = ip;
this.port = port;
}
buildUrlWithCmdAndUserid(cmd, user_id) {
const url = new URL("api/v2", `http://${this.ip}:${this.port}`);
url.searchParams.append("apikey", this.apiKey);
url.searchParams.append("cmd", cmd);
url.searchParams.append("user_id", user_id);
return url;
}
logTautulliError(error) {
console.error("error fetching from tautulli");
throw error;
}
getPlaysByDayOfWeek(plex_userid, days, y_axis) {
const url = this.buildUrlWithCmdAndUserid(
"get_plays_by_dayofweek",
plex_userid
);
url.searchParams.append("time_range", days);
url.searchParams.append("y_axis", y_axis);
return fetch(url.href)
.then(resp => resp.json())
.catch(error => this.logTautulliError(error));
}
getPlaysByDays(plex_userid, days, y_axis) {
const url = this.buildUrlWithCmdAndUserid("get_plays_by_date", plex_userid);
url.searchParams.append("time_range", days);
url.searchParams.append("y_axis", y_axis);
return fetch(url.href)
.then(resp => resp.json())
.catch(error => this.logTautulliError(error));
}
watchTimeStats(plex_userid) {
const url = this.buildUrlWithCmdAndUserid(
"get_user_watch_time_stats",
plex_userid
);
url.searchParams.append("grouping", 0);
return fetch(url.href)
.then(resp => resp.json())
.catch(error => this.logTautulliError(error));
}
viewHistory(plex_userid) {
const url = this.buildUrlWithCmdAndUserid("get_history", plex_userid);
url.searchParams.append("start", 0);
url.searchParams.append("length", 50);
console.log("fetching url", url.href);
return fetch(url.href)
.then(resp => resp.json())
.catch(error => this.logTautulliError(error));
}
}
module.exports = Tautulli;

View File

@@ -0,0 +1,3 @@
{
"presets": ["@babel/preset-env"]
}

View File

@@ -18,12 +18,12 @@ class Cache {
* @returns {Object} * @returns {Object}
*/ */
get(key) { get(key) {
return Promise.resolve() return Promise.resolve()
.then(() => this.database.get(this.queries.read, [key])) .then(() => this.database.get(this.queries.read, [key]))
.then((row) => { .then(row => {
assert(row, 'Could not find cache enrty with that key.'); assert(row, 'Could not find cache entry with that key.');
return JSON.parse(row.value); return JSON.parse(row.value);
}); })
} }
/** /**

View File

@@ -1,49 +0,0 @@
const Movie = require('src/tmdb/types/movie');
const tmdbSwitcher = (tmdbMovie, property) => tmdbMovie[property]
function convertTmdbToMovie(tmdbMovie, credits=undefined) {
const movie = new Movie(tmdbMovie.id, tmdbMovie.title)
movie.overview = tmdbMovie.overview;
movie.rank = tmdbMovie.vote_average;
if (credits) {
movie.credits = credits;
}
if (tmdbMovie.release_date !== undefined) {
movie.release_date = new Date(tmdbMovie.release_date);
movie.year = movie.release_date.getFullYear();
}
if (tmdbMovie.poster_path !== undefined) {
movie.poster = tmdbMovie.poster_path;
}
if (tmdbMovie.backdrop_path !== undefined) {
movie.backdrop = tmdbMovie.backdrop_path;
}
if (tmdbMovie.status !== undefined) {
movie.status = tmdbMovie.status;
}
if (tmdbMovie.genres !== undefined) {
movie.genres = tmdbMovie.genres.map(genre => genre.name);
}
if (tmdbMovie.tagline !== undefined) {
movie.tagline = tmdbMovie.tagline;
}
if (tmdbMovie.runtime !== undefined) {
movie.runtime = tmdbMovie.runtime;
}
if (tmdbMovie.imdb_id !== undefined) {
movie.imdb_id = tmdbMovie.imdb_id;
}
return movie;
}
module.exports = convertTmdbToMovie;

View File

@@ -1,30 +0,0 @@
const Person = require('src/tmdb/types/person');
const convertTmdbToMovie = require('src/tmdb/convertTmdbToMovie');
function convertTmdbToPerson(tmdbPerson, cast=undefined) {
const person = new Person(tmdbPerson.id, tmdbPerson.name);
if (tmdbPerson.profile_path !== undefined) {
person.poster = tmdbPerson.profile_path;
}
if (tmdbPerson.birthday !== undefined) {
person.birthday = new Date(tmdbPerson.birthday);
}
if (tmdbPerson.deathday !== undefined) {
person.deathday = tmdbPerson.deathday;
}
if (tmdbPerson.known_for !== undefined) {
person.known_for = tmdbPerson.known_for.map(convertTmdbToMovie);
}
if (cast) {
person.cast = cast.cast;
}
return person;
}
module.exports = convertTmdbToPerson;

View File

@@ -1,62 +0,0 @@
const TMDB = require('src/media_classes/tmdb');
const Movie = require('src/types/movie');
function translateYear(tmdbReleaseDate) {
return new Date(tmdbReleaseDate).getFullYear();
}
function translateGenre(tmdbGenres) {
return tmdbGenres.map(genre => genre.name);
}
function convertType(tmdbType) {
if (tmdbType === 'tv') return 'show';
return undefined;
}
function convertTmdbToMovie(tmdb) {
const year =
const movie = new Movie();
let seasoned = undefined;
if (tmdb.id && tmdb.title && tmdb.release_date) {
const year = tmdb.release_date.getFullYear();
seasoned = new Movie(tmdb.id, tmdb.title, year);
}
else if (tmdb.id && tmdb.name && tmdb.first_air_date) {
const year = tmdb.first_air_date.getFullYear();
seasoned = new Show(tmdb.id, tmdb.name, year);
seasoned.seasons = tmdb.number_of_season;
seasoned.episodes = tmdb.episodes;
return
}
}
const title = tmdb.title || tmdb.name;
const year = translateYear(tmdb.release_date || tmdb.first_air_date);
const type = manualType || convertType(tmdb.type) || 'movie';
const id = tmdb.id;
const summary = tmdb.overview;
const poster_path = tmdb.poster_path;
const background_path = tmdb.backdrop_path;
const popularity = tmdb.popularity;
const score = tmdb.vote_average;
// const genres = translateGenre(tmdb.genres);
const release_status = tmdb.status;
const tagline = tmdb.tagline;
const seasons = tmdb.number_of_seasons;
const episodes = tmdb.episodes;
const seasoned = new TMDB(
title, year, type, id, summary, poster_path, background_path,
popularity, score, release_status, tagline, seasons, episodes
);
// seasoned.print()
return seasoned;
}
module.exports = convertTmdbToSeasoned;

View File

@@ -1,41 +0,0 @@
const Show = require('src/tmdb/types/show');
function convertTmdbToShow(tmdbShow, credits=undefined) {
const show = new Show(tmdbShow.id, tmdbShow.name)
show.seasons = tmdbShow.number_of_seasons;
show.episodes = tmdbShow.number_of_episodes;
show.overview = tmdbShow.overview;
show.rank = tmdbShow.vote_average;
if (credits) {
show.credits = credits
}
if (tmdbShow.genres !== undefined) {
show.genres = tmdbShow.genres.map(genre => genre.name);
}
if (tmdbShow.first_air_date !== undefined) {
show.first_air_date = new Date(tmdbShow.first_air_date);
show.year = show.first_air_date.getFullYear();
}
if (tmdbShow.poster_path !== undefined) {
show.poster = tmdbShow.poster_path;
}
if (tmdbShow.backdrop_path !== undefined) {
show.backdrop = tmdbShow.backdrop_path;
}
if (tmdbShow.status !== undefined) {
show.status = tmdbShow.status;
}
if (tmdbShow.episode_run_time !== undefined) {
show.runtime = tmdbShow.runtime;
}
return show;
}
module.exports = convertTmdbToShow;

View File

@@ -1,94 +1,118 @@
const moviedb = require('km-moviedb'); const moviedb = require("km-moviedb");
const convertTmdbToMovie = require('src/tmdb/convertTmdbToMovie'); const RedisCache = require("src/cache/redis");
const convertTmdbToShow = require('src/tmdb/convertTmdbToShow'); const redisCache = new RedisCache();
const convertTmdbToPerson = require('src/tmdb/convertTmdbToPerson');
class TMDB { const {
constructor(cache, apiKey, tmdbLibrary) { Movie,
this.cache = cache; Show,
this.tmdbLibrary = tmdbLibrary || moviedb(apiKey); Person,
this.cacheTags = { Credits,
multiSearch: 'mus', ReleaseDates
movieSearch: 'mos', } = require("src/tmdb/types");
showSearch: 'ss',
personSearch: 'ps', const tmdbErrorResponse = (error, typeString = undefined) => {
movieInfo: 'mi', if (error.status === 404) {
showInfo: 'si', let message = error.response.body.status_message;
personInfo: 'pi',
miscNowPlayingMovies: 'npm', throw {
miscPopularMovies: 'pm', status: 404,
miscTopRatedMovies: 'tpm', message: message.slice(0, -1) + " in tmdb."
miscUpcomingMovies: 'um', };
tvOnTheAir: 'toa', } else if (error.status === 401) {
miscPopularTvs: 'pt', throw {
miscTopRatedTvs: 'trt', status: 401,
message: error.response.body.status_message
}; };
} }
/** throw {
* Retrieve a specific movie by id from TMDB. status: 500,
* @param {Number} identifier of the movie you want to retrieve message: `An unexpected error occured while fetching ${typeString} from tmdb`
* @param {String} type filter results by type (default movie). };
* @returns {Promise} succeeds if movie was found };
*/
lookup(identifier, type = 'movie') {
const query = { id: identifier };
const cacheKey = `${this.cacheTags.info}:${type}:${identifier}`;
return Promise.resolve()
.then(() => this.cache.get(cacheKey))
.catch(() => this.tmdb(TMDB_METHODS['info'][type], query))
.catch(() => { throw new Error('Could not find a movie with that id.'); })
.then(response => this.cache.set(cacheKey, response))
.then(response => {
try {
return convertTmdbToSeasoned(response, type);
} catch (parseError) {
console.error(parseError);
throw new Error('Could not parse movie.');
}
});
}
/** class TMDB {
* Retrive search results from TMDB. constructor(apiKey, cache, tmdbLibrary) {
* @param {String} text query you want to search for this.tmdbLibrary = tmdbLibrary || moviedb(apiKey);
* @param {Number} page representing pagination of results
* @param {String} type filter results by type (default multi) this.cache = cache || redisCache;
* @returns {Promise} dict with query results, current page and total_pages this.cacheTags = {
*/ multiSearch: "mus",
search(text, page = 1, type = 'multi') { movieSearch: "mos",
const query = { query: text, page: page }; showSearch: "ss",
const cacheKey = `${this.cacheTags.search}:${page}:${type}:${text}`; personSearch: "ps",
return Promise.resolve() movieInfo: "mi",
.then(() => this.cache.get(cacheKey)) movieCredits: "mc",
.catch(() => this.tmdb(TMDB_METHODS['search'][type], query)) movieReleaseDates: "mrd",
.catch(() => { throw new Error('Could not search for movies/shows at tmdb.'); }) movieImages: "mimg",
.then(response => this.cache.set(cacheKey, response)) showInfo: "si",
.then(response => this.mapResults(response)); showCredits: "sc",
} personInfo: "pi",
personCredits: "pc",
miscNowPlayingMovies: "npm",
miscPopularMovies: "pm",
miscTopRatedMovies: "tpm",
miscUpcomingMovies: "um",
tvOnTheAir: "toa",
miscPopularTvs: "pt",
miscTopRatedTvs: "trt"
};
this.defaultTTL = 86400;
}
getFromCacheOrFetchFromTmdb(cacheKey, tmdbMethod, query) {
return new Promise((resolve, reject) =>
this.cache
.get(cacheKey)
.then(resolve)
.catch(() => this.tmdb(tmdbMethod, query))
.then(resolve)
.catch(error => reject(tmdbErrorResponse(error, tmdbMethod)))
);
}
/** /**
* Retrieve a specific movie by id from TMDB. * Retrieve a specific movie by id from TMDB.
* @param {Number} identifier of the movie you want to retrieve * @param {Number} identifier of the movie you want to retrieve
* @param {String} type filter results by type (default movie). * @param {Boolean} add credits (cast & crew) for movie
* @param {Boolean} add release dates for every country
* @returns {Promise} succeeds if movie was found * @returns {Promise} succeeds if movie was found
*/ */
movieInfo(identifier, credits=false) { movieInfo(identifier) {
const query = { id: identifier }; const query = { id: identifier };
const cacheKey = `${this.cacheTags.movieInfo}:${identifier}:${credits}`; const cacheKey = `tmdb/${this.cacheTags.movieInfo}:${identifier}`;
const requests = [this.tmdb('movieInfo', query)] return this.getFromCacheOrFetchFromTmdb(cacheKey, "movieInfo", query)
.then(movie => this.cache.set(cacheKey, movie, this.defaultTTL))
.then(movie => Movie.convertFromTmdbResponse(movie));
}
if (credits) { /**
requests.push(this.tmdb('movieCredits', query)) * Retrieve credits for a movie
} * @param {Number} identifier of the movie to get credits for
* @returns {Promise} movie cast object
*/
movieCredits(identifier) {
const query = { id: identifier };
const cacheKey = `tmdb/${this.cacheTags.movieCredits}:${identifier}`;
return Promise.resolve() return this.getFromCacheOrFetchFromTmdb(cacheKey, "movieCredits", query)
.then(() => this.cache.get(cacheKey)) .then(credits => this.cache.set(cacheKey, credits, this.defaultTTL))
.catch(() => Promise.all(requests)) .then(credits => Credits.convertFromTmdbResponse(credits));
.catch((error) => { console.log(error); throw new Error('Could not find a movie with that id.'); }) }
.then(([movies, credits]) => this.cache.set(cacheKey, [movies, credits]))
.then(([movies, credits]) => convertTmdbToMovie(movies, credits)) /**
* Retrieve release dates for a movie
* @param {Number} identifier of the movie to get release dates for
* @returns {Promise} movie release dates object
*/
movieReleaseDates(identifier) {
const query = { id: identifier }
const cacheKey = `tmdb/${this.cacheTags.movieReleaseDates}:${identifier}`
return this.getFromCacheOrFetchFromTmdb(cacheKey, 'movieReleaseDates', query)
.then(releaseDates => this.cache.set(cacheKey, releaseDates, this.defaultTTL))
.then(releaseDates => ReleaseDates.convertFromTmdbResponse(releaseDates))
} }
/** /**
@@ -97,22 +121,22 @@ class TMDB {
* @param {String} type filter results by type (default show). * @param {String} type filter results by type (default show).
* @returns {Promise} succeeds if show was found * @returns {Promise} succeeds if show was found
*/ */
showInfo(identifier, credits=false) { showInfo(identifier) {
const query = { id: identifier }; const query = { id: identifier };
const cacheKey = `${this.cacheTags.showInfo}:${identifier}:${credits}`; const cacheKey = `tmdb/${this.cacheTags.showInfo}:${identifier}`;
const requests = [this.tmdb('tvInfo', query)] return this.getFromCacheOrFetchFromTmdb(cacheKey, "tvInfo", query)
.then(show => this.cache.set(cacheKey, show, this.defaultTTL))
.then(show => Show.convertFromTmdbResponse(show));
}
if (credits) { showCredits(identifier) {
requests.push(this.tmdb('tvCredits', query)) const query = { id: identifier };
} const cacheKey = `tmdb/${this.cacheTags.showCredits}:${identifier}`;
return Promise.resolve() return this.getFromCacheOrFetchFromTmdb(cacheKey, "tvCredits", query)
.then(() => this.cache.get(cacheKey)) .then(credits => this.cache.set(cacheKey, credits, this.defaultTTL))
.catch(() => Promise.all(requests)) .then(credits => Credits.convertFromTmdbResponse(credits));
.catch(() => { throw new Error('Could not find a show with that id.'); })
.then(([shows, credits]) => this.cache.set(cacheKey, [shows, credits]))
.then(([shows, credits]) => convertTmdbToShow(shows, credits))
} }
/** /**
@@ -123,27 +147,32 @@ class TMDB {
*/ */
personInfo(identifier) { personInfo(identifier) {
const query = { id: identifier }; const query = { id: identifier };
const cacheKey = `${this.cacheTags.personInfo}:${identifier}`; const cacheKey = `tmdb/${this.cacheTags.personInfo}:${identifier}`;
return Promise.resolve() return this.getFromCacheOrFetchFromTmdb(cacheKey, "personInfo", query)
.then(() => this.cache.get(cacheKey)) .then(person => this.cache.set(cacheKey, person, this.defaultTTL))
.catch(() => Promise.all([this.tmdb('personInfo', query), this.tmdb('personCombinedCredits', query)])) .then(person => Person.convertFromTmdbResponse(person));
.catch(() => { throw new Error('Could not find a person with that id.'); })
.then(([person, cast]) => this.cache.set(cacheKey, [person, cast]))
.then(([person, cast]) => convertTmdbToPerson(person, cast))
.catch(err => new Error('Unable to convert result to person', err))
} }
personCredits(identifier) {
const query = { id: identifier };
const cacheKey = `tmdb/${this.cacheTags.personCredits}:${identifier}`;
return this.getFromCacheOrFetchFromTmdb(
cacheKey,
"personCombinedCredits",
query
)
.then(credits => this.cache.set(cacheKey, credits, this.defaultTTL))
.then(credits => Credits.convertFromTmdbResponse(credits));
}
multiSearch(search_query, page=1) { multiSearch(search_query, page = 1, include_adult = true) {
const query = { query: search_query, page: page }; const query = { query: search_query, page, include_adult };
const cacheKey = `${this.cacheTags.multiSearch}:${page}:${search_query}`; const cacheKey = `tmdb/${this.cacheTags.multiSearch}:${page}:${search_query}:${include_adult}`;
return Promise.resolve()
.then(() => this.cache.get(cacheKey)) return this.getFromCacheOrFetchFromTmdb(cacheKey, "searchMulti", query)
.catch(() => this.tmdb('searchMulti', query)) .then(response => this.cache.set(cacheKey, response, this.defaultTTL))
.catch(() => { throw new Error('Could not complete search to tmdb'); })
.then(response => this.cache.set(cacheKey, response))
.then(response => this.mapResults(response)); .then(response => this.mapResults(response));
} }
@@ -153,17 +182,13 @@ class TMDB {
* @param {Number} page representing pagination of results * @param {Number} page representing pagination of results
* @returns {Promise} dict with query results, current page and total_pages * @returns {Promise} dict with query results, current page and total_pages
*/ */
movieSearch(query, page=1) { movieSearch(search_query, page = 1, include_adult = true) {
const tmdbquery = { query: query, page: page }; const tmdbquery = { query: search_query, page, include_adult };
const cacheKey = `${this.cacheTags.movieSearch}:${page}:${query}`; const cacheKey = `tmdb/${this.cacheTags.movieSearch}:${page}:${search_query}:${include_adult}`;
return Promise.resolve() return this.getFromCacheOrFetchFromTmdb(cacheKey, "searchMovie", tmdbquery)
.then(() => this.cache.get(cacheKey)) .then(response => this.cache.set(cacheKey, response, this.defaultTTL))
.catch(() => this.tmdb('searchMovie', tmdbquery)) .then(response => this.mapResults(response, "movie"));
.catch(() => { throw new Error('Could not complete movie search to tmdb'); })
.then(response => this.cache.set(cacheKey, response))
.then(response => this.mapAndCreateResponse(response, convertTmdbToMovie))
.catch((error) => { console.log(error); throw new Error('Could not parse movie search result') })
} }
/** /**
@@ -172,16 +197,13 @@ class TMDB {
* @param {Number} page representing pagination of results * @param {Number} page representing pagination of results
* @returns {Promise} dict with query results, current page and total_pages * @returns {Promise} dict with query results, current page and total_pages
*/ */
showSearch(query, page=1) { showSearch(search_query, page = 1, include_adult = true) {
const tmdbquery = { query: query, page: page }; const tmdbquery = { query: search_query, page, include_adult };
const cacheKey = `${this.cacheTags.showSearch}:${page}:${query}`; const cacheKey = `tmdb/${this.cacheTags.showSearch}:${page}:${search_query}:${include_adult}`;
return Promise.resolve()
.then(() => this.cache.get(cacheKey)) return this.getFromCacheOrFetchFromTmdb(cacheKey, "searchTv", tmdbquery)
.catch(() => this.tmdb('searchTv', tmdbquery)) .then(response => this.cache.set(cacheKey, response, this.defaultTTL))
.catch(() => { throw new Error('Could not complete show search to tmdb'); }) .then(response => this.mapResults(response, "show"));
.then(response => this.cache.set(cacheKey, response))
.then(response => this.mapAndCreateResponse(response, convertTmdbToShow))
.catch((error) => { console.log(error); throw new Error('Could not parse show search result') })
} }
/** /**
@@ -190,79 +212,31 @@ class TMDB {
* @param {Number} page representing pagination of results * @param {Number} page representing pagination of results
* @returns {Promise} dict with query results, current page and total_pages * @returns {Promise} dict with query results, current page and total_pages
*/ */
personSearch(query, page=1) { personSearch(search_query, page = 1, include_adult = true) {
const tmdbquery = { query: query, page: page }; const tmdbquery = { query: search_query, page, include_adult };
const cacheKey = `${this.cacheTags.personSearch}:${page}:${query}`; const cacheKey = `tmdb/${this.cacheTags.personSearch}:${page}:${search_query}:${include_adult}`;
return Promise.resolve()
.then(() => this.cache.get(cacheKey))
.catch(() => this.tmdb('searchPerson', tmdbquery))
.catch(() => { throw new Error('Could not complete person search to tmdb'); })
.then(response => this.cache.set(cacheKey, response))
.then(response => this.mapAndCreateResponse(response, convertTmdbToPerson))
.catch((error) => { console.log(error); throw new Error('Could not parse person search result') })
}
mapAndCreateResponse(response, resultConvertFunction) { return this.getFromCacheOrFetchFromTmdb(cacheKey, "searchPerson", tmdbquery)
// console.log(response) .then(response => this.cache.set(cacheKey, response, this.defaultTTL))
return { .then(response => this.mapResults(response, "person"));
results: response.results.map(resultConvertFunction),
page: response.page,
total_results: response.total_results,
total_pages: response.total_pages
}
} }
movieList(listname, page = 1) { movieList(listname, page = 1) {
const query = { page: page }; const query = { page: page };
const cacheKey = `${this.cacheTags[listname]}:${page}`; const cacheKey = `tmdb/${this.cacheTags[listname]}:${page}`;
return Promise.resolve()
.then(() => this.cache.get(cacheKey)) return this.getFromCacheOrFetchFromTmdb(cacheKey, listname, query)
.catch(() => this.tmdb(listname, query)) .then(response => this.cache.set(cacheKey, response, this.defaultTTL))
.catch(() => { throw new Error('Unable to get movie list from tmdb')}) .then(response => this.mapResults(response, "movie"));
.then(response => this.cache.set(cacheKey, response))
.then(response => this.mapAndCreateResponse(response, convertTmdbToMovie));
} }
showList(listname, page = 1) { showList(listname, page = 1) {
const query = { page: page }; const query = { page: page };
const cacheKey = `${this.cacheTags[listname]}:${page}`; const cacheKey = `tmdb/${this.cacheTags[listname]}:${page}`;
return Promise.resolve()
.then(() => this.cache.get(cacheKey))
.catch(() => this.tmdb(listname, query))
.catch(() => { throw new Error('Unable to get show list from tmdb')})
.then(response => this.cache.set(cacheKey, response))
.then(response => this.mapAndCreateResponse(response, convertTmdbToShow));
}
/** return this.getFromCacheOrFetchFromTmdb(cacheKey, listName, query)
* Fetches a given list from tmdb. .then(response => this.cache.set(cacheKey, response, this.defaultTTL))
* @param {String} listName Name of list .then(response => this.mapResults(response, "show"));
* @param {String} type filter results by type (default movie)
* @param {Number} page representing pagination of results
* @returns {Promise} dict with query results, current page and total_pages
*/
listSearch(listName, type = 'movie', page = '1') {
const query = { page: page };
console.log(query);
const cacheKey = `${this.cacheTags[listName]}:${type}:${page}`;
return Promise.resolve()
.then(() => this.cache.get(cacheKey))
.catch(() => this.tmdb(TMDB_METHODS[listName][type], query))
.catch(() => { throw new Error('Error fetching list from tmdb.'); })
.then(response => this.cache.set(cacheKey, response))
.then(response => this.mapResults(response, type));
}
popular(type='movie', page=1) {
const query = { type: type, page: page };
const cacheKey = `${this.cacheTags.popular}:${type}:${page}`;
return Promise.resolve()
.then(() => this.cache.get(cacheKey))
.catch(() => this.tmdb('miscPopularMovies', query))
.catch((e) => { throw new Error(`Error fetching popular list of type ${type} : ${e}`) })
.then(response => this.cache.set(cacheKey, response))
.then(response => this.mapResults(response, type));
} }
/** /**
@@ -271,23 +245,26 @@ class TMDB {
* @param {String} The type declared in listSearch. * @param {String} The type declared in listSearch.
* @returns {Promise} dict with tmdb results, mapped as movie/show objects. * @returns {Promise} dict with tmdb results, mapped as movie/show objects.
*/ */
mapResults(response, _) { mapResults(response, type = undefined) {
let results = response.results.map((result) => { let results = response.results.map(result => {
if (result.media_type === 'movie') { if (type === "movie" || result.media_type === "movie") {
return convertTmdbToMovie(result); const movie = Movie.convertFromTmdbResponse(result);
} else if (result.media_type === 'tv') { return movie.createJsonResponse();
return convertTmdbToShow(result); } else if (type === "show" || result.media_type === "tv") {
} else if (result.media_type === 'person') { const show = Show.convertFromTmdbResponse(result);
return convertTmdbToPerson(result); return show.createJsonResponse();
} else if (type === "person" || result.media_type === "person") {
const person = Person.convertFromTmdbResponse(result);
return person.createJsonResponse();
} }
}) });
return { return {
results: results, results: results,
page: response.page, page: response.page,
total_results: response.total_results, total_results: response.total_results,
total_pages: response.total_pages total_pages: response.total_pages
} };
} }
/** /**
@@ -296,22 +273,22 @@ class TMDB {
* @param {Object} argument argument to function being called * @param {Object} argument argument to function being called
* @returns {Promise} succeeds if callback succeeds * @returns {Promise} succeeds if callback succeeds
*/ */
tmdb(method, argument) { tmdb(method, argument) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const callback = (error, reponse) => { const callback = (error, reponse) => {
if (error) { if (error) {
return reject(error); return reject(error);
} }
resolve(reponse); resolve(reponse);
}; };
if (!argument) { if (!argument) {
this.tmdbLibrary[method](callback); this.tmdbLibrary[method](callback);
} else { } else {
this.tmdbLibrary[method](argument, callback); this.tmdbLibrary[method](argument, callback);
} }
}); });
} }
} }
module.exports = TMDB; module.exports = TMDB;

View File

@@ -0,0 +1,7 @@
import { Movie } from './types'
Movie('str', 123)
module.exports = TMDB;

View File

@@ -0,0 +1,7 @@
const Movie = require('./types/movie.js')
const Show = require('./types/show.js')
const Person = require('./types/person.js')
const Credits = require('./types/credits.js')
const ReleaseDates = require('./types/releaseDates.js')
module.exports = { Movie, Show, Person, Credits, ReleaseDates }

View File

@@ -0,0 +1,64 @@
interface Movie {
adult: boolean;
backdrop: string;
genres: Genre[];
id: number;
imdb_id: number;
overview: string;
popularity: number;
poster: string;
release_date: Date;
rank: number;
runtime: number;
status: string;
tagline: string;
title: string;
vote_count: number;
}
interface Show {
adult: boolean;
backdrop: string;
episodes: number;
genres: Genre[];
id: number;
imdb_id: number;
overview: string;
popularity: number;
poster: string;
rank: number;
runtime: number;
seasons: number;
status: string;
tagline: string;
title: string;
vote_count: number;
}
interface Person {
birthday: Date;
deathday: Date;
id: number;
known_for: string;
name: string;
poster: string;
}
interface SearchResult {
adult: boolean;
backdrop_path: string;
id: number;
original_title: string;
release_date: Date;
poster_path: string;
popularity: number;
vote_average: number;
vote_counte: number;
}
interface Genre {
id: number;
name: string;
}
export { Movie, Show, Person, Genre }

View File

@@ -0,0 +1,113 @@
import Movie from "./movie";
import Show from "./show";
class Credits {
constructor(id, cast = [], crew = []) {
this.id = id;
this.cast = cast;
this.crew = crew;
this.type = "credits";
}
static convertFromTmdbResponse(response) {
const { id, cast, crew } = response;
const allCast = cast.map(cast => {
if (cast["media_type"]) {
if (cast.media_type === "movie") {
return CreditedMovie.convertFromTmdbResponse(cast);
} else if (cast.media_type === "tv") {
return CreditedShow.convertFromTmdbResponse(cast);
}
}
return new CastMember(
cast.character,
cast.gender,
cast.id,
cast.name,
cast.profile_path
);
});
const allCrew = crew.map(crew => {
if (cast["media_type"]) {
if (cast.media_type === "movie") {
return CreditedMovie.convertFromTmdbResponse(cast);
} else if (cast.media_type === "tv") {
return CreditedShow.convertFromTmdbResponse(cast);
}
}
return new CrewMember(
crew.department,
crew.gender,
crew.id,
crew.job,
crew.name,
crew.profile_path
);
});
return new Credits(id, allCast, allCrew);
}
createJsonResponse() {
return {
id: this.id,
cast: this.cast.map(cast => cast.createJsonResponse()),
crew: this.crew.map(crew => crew.createJsonResponse())
};
}
}
class CastMember {
constructor(character, gender, id, name, profile_path) {
this.character = character;
this.gender = gender;
this.id = id;
this.name = name;
this.profile_path = profile_path;
this.type = "person";
}
createJsonResponse() {
return {
character: this.character,
gender: this.gender,
id: this.id,
name: this.name,
profile_path: this.profile_path,
type: this.type
};
}
}
class CrewMember {
constructor(department, gender, id, job, name, profile_path) {
this.department = department;
this.gender = gender;
this.id = id;
this.job = job;
this.name = name;
this.profile_path = profile_path;
this.type = "person";
}
createJsonResponse() {
return {
department: this.department,
gender: this.gender,
id: this.id,
job: this.job,
name: this.name,
profile_path: this.profile_path,
type: this.type
};
}
}
class CreditedMovie extends Movie {}
class CreditedShow extends Show {}
module.exports = Credits;

View File

@@ -1,20 +1,62 @@
class Movie { class Movie {
constructor(id, title, year=null, overview=null, poster=null, backdrop=null, rank=null, genres=null, status=null, constructor(id, title, year=undefined, overview=undefined, poster=undefined, backdrop=undefined,
tagline=null, runtime=null, imdb_id=null) { releaseDate=undefined, rating=undefined, genres=undefined, productionStatus=undefined,
tagline=undefined, runtime=undefined, imdb_id=undefined, popularity=undefined) {
this.id = id; this.id = id;
this.title = title; this.title = title;
this.year = year; this.year = year;
this.overview = overview; this.overview = overview;
this.poster = poster; this.poster = poster;
this.backdrop = backdrop; this.backdrop = backdrop;
this.rank = rank; this.releaseDate = releaseDate;
this.rating = rating;
this.genres = genres; this.genres = genres;
this.status = status; this.productionStatus = productionStatus;
this.tagline = tagline; this.tagline = tagline;
this.runtime = runtime; this.runtime = runtime;
this.imdb_id = imdb_id; this.imdb_id = imdb_id;
this.popularity = popularity;
this.type = 'movie'; this.type = 'movie';
} }
static convertFromTmdbResponse(response) {
const { id, title, release_date, overview, poster_path, backdrop_path, vote_average, genres, status,
tagline, runtime, imdb_id, popularity } = response;
const releaseDate = new Date(release_date);
const year = releaseDate.getFullYear();
const genreNames = genres ? genres.map(g => g.name) : undefined
return new Movie(id, title, year, overview, poster_path, backdrop_path, releaseDate, vote_average, genreNames, status,
tagline, runtime, imdb_id, popularity)
}
static convertFromPlexResponse(response) {
// console.log('response', response)
const { title, year, rating, tagline, summary } = response;
const _ = undefined
return new Movie(null, title, year, summary, _, _, _, rating, _, _, tagline)
}
createJsonResponse() {
return {
id: this.id,
title: this.title,
year: this.year,
overview: this.overview,
poster: this.poster,
backdrop: this.backdrop,
release_date: this.releaseDate,
rating: this.rating,
genres: this.genres,
production_status: this.productionStatus,
tagline: this.tagline,
runtime: this.runtime,
imdb_id: this.imdb_id,
type: this.type
}
}
} }
module.exports = Movie; module.exports = Movie;

View File

@@ -1,12 +1,69 @@
class Person { class Person {
constructor(id, name, poster=null, birthday=null, deathday=null, known_for=null) { constructor(
id,
name,
poster = undefined,
birthday = undefined,
deathday = undefined,
adult = undefined,
placeOfBirth = undefined,
biography = undefined,
knownForDepartment = undefined
) {
this.id = id; this.id = id;
this.name = name; this.name = name;
this.poster = poster; this.poster = poster;
this.birthday = birthday; this.birthday = birthday;
this.deathday = deathday; this.deathday = deathday;
this.known_for = known_for; this.adult = adult;
this.type = 'person'; this.placeOfBirth = placeOfBirth;
this.biography = biography;
this.knownForDepartment = knownForDepartment;
this.type = "person";
}
static convertFromTmdbResponse(response) {
const {
id,
name,
profile_path,
birthday,
deathday,
adult,
place_of_birth,
biography,
known_for_department
} = response;
const birthDay = new Date(birthday);
const deathDay = deathday ? new Date(deathday) : null;
return new Person(
id,
name,
profile_path,
birthDay,
deathDay,
adult,
place_of_birth,
biography,
known_for_department
);
}
createJsonResponse() {
return {
id: this.id,
name: this.name,
poster: this.poster,
birthday: this.birthday,
deathday: this.deathday,
place_of_birth: this.placeOfBirth,
biography: this.biography,
known_for_department: this.knownForDepartment,
adult: this.adult,
type: this.type
};
} }
} }

View File

@@ -0,0 +1,78 @@
class ReleaseDates {
constructor(id, releases) {
this.id = id;
this.releases = releases;
}
static convertFromTmdbResponse(response) {
const { id, results } = response;
const releases = results.map(countryRelease =>
new Release(
countryRelease.iso_3166_1,
countryRelease.release_dates.map(rd => new ReleaseDate(rd.certification, rd.iso_639_1, rd.release_date, rd.type, rd.note))
))
return new ReleaseDates(id, releases)
}
createJsonResponse() {
return {
id: this.id,
results: this.releases.map(release => release.createJsonResponse())
}
}
}
class Release {
constructor(country, releaseDates) {
this.country = country;
this.releaseDates = releaseDates;
}
createJsonResponse() {
return {
country: this.country,
release_dates: this.releaseDates.map(releaseDate => releaseDate.createJsonResponse())
}
}
}
class ReleaseDate {
constructor(certification, language, releaseDate, type, note) {
this.certification = certification;
this.language = language;
this.releaseDate = releaseDate;
this.type = this.releaseTypeLookup(type);
this.note = note;
}
releaseTypeLookup(releaseTypeKey) {
const releaseTypeEnum = {
1: 'Premier',
2: 'Limited theatrical',
3: 'Theatrical',
4: 'Digital',
5: 'Physical',
6: 'TV'
}
if (releaseTypeKey <= Object.keys(releaseTypeEnum).length) {
return releaseTypeEnum[releaseTypeKey]
} else {
// TODO log | Release type not defined, does this need updating?
return null
}
}
createJsonResponse() {
return {
certification: this.certification,
language: this.language,
release_date: this.releaseDate,
type: this.type,
note: this.note
}
}
}
module.exports = ReleaseDates;

View File

@@ -1,20 +1,50 @@
class Show { class Show {
constructor(id, title, year=null, seasons=null, episodes=null, overview=null, rank=null, genres=null, constructor(id, title, year=undefined, overview=undefined, poster=undefined, backdrop=undefined,
poster=null, backdrop=null, status=null, runtime=null) { seasons=undefined, episodes=undefined, rank=undefined, genres=undefined, status=undefined,
runtime=undefined) {
this.id = id; this.id = id;
this.title = title; this.title = title;
this.year = year; this.year = year;
this.seasons = seasons;
this.episodes = episodes;
this.overview = overview; this.overview = overview;
this.rank = rank;
this.genres = genres;
this.poster = poster; this.poster = poster;
this.backdrop = backdrop; this.backdrop = backdrop;
this.status = status; this.seasons = seasons;
this.episodes = episodes;
this.rank = rank;
this.genres = genres;
this.productionStatus = status;
this.runtime = runtime; this.runtime = runtime;
this.type = 'show'; this.type = 'show';
} }
static convertFromTmdbResponse(response) {
const { id, name, first_air_date, overview, poster_path, backdrop_path, number_of_seasons, number_of_episodes,
rank, genres, status, episode_run_time, popularity } = response;
const year = new Date(first_air_date).getFullYear()
const genreNames = genres ? genres.map(g => g.name) : undefined
return new Show(id, name, year, overview, poster_path, backdrop_path, number_of_seasons, number_of_episodes,
rank, genreNames, status, episode_run_time, popularity)
}
createJsonResponse() {
return {
id: this.id,
title: this.title,
year: this.year,
overview: this.overview,
poster: this.poster,
backdrop: this.backdrop,
seasons: this.seasons,
episodes: this.episodes,
rank: this.rank,
genres: this.genres,
production_status: this.productionStatus,
runtime: this.runtime,
type: this.type
}
}
} }
module.exports = Show; module.exports = Show;

View File

@@ -1,37 +1,41 @@
const User = require('src/user/user'); const User = require("src/user/user");
const jwt = require('jsonwebtoken'); const jwt = require("jsonwebtoken");
class Token { class Token {
constructor(user) { constructor(user, admin = false, settings = null) {
this.user = user; this.user = user;
} this.admin = admin;
this.settings = settings;
}
/** /**
* Generate a new token. * Generate a new token.
* @param {String} secret a cipher of the token * @param {String} secret a cipher of the token
* @returns {String} * @returns {String}
*/ */
toString(secret) { toString(secret) {
return jwt.sign({ username: this.user.username }, secret); const { user, admin, settings } = this;
}
/** let data = { username: user.username, settings };
if (admin) data["admin"] = admin;
return jwt.sign(data, secret, { expiresIn: "90d" });
}
/**
* Decode a token. * Decode a token.
* @param {Token} jwtToken an encrypted token * @param {Token} jwtToken an encrypted token
* @param {String} secret a cipher of the token * @param {String} secret a cipher of the token
* @returns {Token} * @returns {Token}
*/ */
static fromString(jwtToken, secret) { static fromString(jwtToken, secret) {
let username = null; const token = jwt.verify(jwtToken, secret, { clockTolerance: 10000 });
if (token.username == null) throw new Error("Malformed token");
try { const { username, admin, settings } = token;
username = jwt.verify(jwtToken, secret).username; const user = new User(username);
} catch (error) { return new Token(user, admin, settings);
throw new Error('The token is invalid.'); }
}
const user = new User(username);
return new Token(user);
}
} }
module.exports = Token; module.exports = Token;

View File

@@ -1,64 +1,256 @@
const assert = require('assert'); const assert = require("assert");
const establishedDatabase = require('src/database/database'); const establishedDatabase = require("src/database/database");
class UserRepository { class UserRepository {
constructor(database) { constructor(database) {
this.database = database || establishedDatabase; this.database = database || establishedDatabase;
this.queries = { this.queries = {
read: 'select * from user where lower(user_name) = lower(?)', read: "select * from user where lower(user_name) = lower(?)",
create: 'insert into user (user_name) values (?)', create: "insert into user (user_name) values (?)",
change: 'update user set password = ? where user_name = ?', change: "update user set password = ? where user_name = ?",
retrieveHash: 'select * from user where user_name = ?', retrieveHash: "select * from user where user_name = ?",
getAdminStateByUser: 'select admin from user where user_name = ?' getAdminStateByUser: "select admin from user where user_name = ?",
}; link: "update settings set plex_userid = ? where user_name = ?",
} unlink: "update settings set plex_userid = null where user_name = ?",
createSettings: "insert into settings (user_name) values (?)",
updateSettings:
"update settings set user_name = ?, dark_mode = ?, emoji = ?",
getSettings: "select * from settings where user_name = ?"
};
}
/** /**
* Create a user in a database. * Create a user in a database.
* @param {User} user the user you want to create * @param {User} user the user you want to create
* @returns {Promise} * @returns {Promise}
*/ */
create(user) { create(user) {
return Promise.resolve() return this.database
.then(() => this.database.get(this.queries.read, user.username)) .get(this.queries.read, user.username)
.then(() => this.database.run(this.queries.create, user.username)) .then(() => this.database.run(this.queries.create, user.username))
.catch((error) => { .catch(error => {
if (error.name === 'AssertionError' || error.message.endsWith('user_name')) { if (
throw new Error('That username is already registered'); error.name === "AssertionError" ||
} error.message.endsWith("user_name")
}); ) {
} throw new Error("That username is already registered");
}
throw Error(error);
});
}
/** /**
* Retrieve a password from a database. * Retrieve a password from a database.
* @param {User} user the user you want to retrieve the password * @param {User} user the user you want to retrieve the password
* @returns {Promise} * @returns {Promise}
*/ */
retrieveHash(user) { retrieveHash(user) {
return Promise.resolve() return this.database
.then(() => this.database.get(this.queries.retrieveHash, user.username)) .get(this.queries.retrieveHash, user.username)
.then((row) => { .then(row => {
assert(row, 'The user does not exist.'); assert(row, "The user does not exist.");
return row.password; return row.password;
}) })
.catch((err) => { console.log(error); throw new Error('Unable to find your user.'); }); .catch(err => {
} console.log(error);
throw new Error("Unable to find your user.");
});
}
/** /**
* Change a user's password in a database. * Change a user's password in a database.
* @param {User} user the user you want to create * @param {User} user the user you want to create
* @param {String} password the new password you want to change * @param {String} password the new password you want to change
* @returns {Promise} * @returns {Promise}
*/ */
changePassword(user, password) { changePassword(user, password) {
return Promise.resolve(this.database.run(this.queries.change, [password, user.username])); return this.database.run(this.queries.change, [password, user.username]);
} }
checkAdmin(user) { /**
return this.database.get(this.queries.getAdminStateByUser, user.username).then((row) => { * Link plex userid with seasoned user
return row.admin; * @param {String} username the user you want to lunk plex userid with
}); * @param {Number} plexUserID plex unique id
} * @returns {Promsie}
*/
linkPlexUserId(username, plexUserID) {
return new Promise((resolve, reject) => {
this.database
.run(this.queries.link, [plexUserID, username])
.then(row => resolve(row))
.catch(error => {
// TODO log this unknown db error
console.error("db error", error);
reject({
status: 500,
message:
"An unexpected error occured while linking plex and seasoned accounts",
source: "seasoned database"
});
});
});
}
/**
* Unlink plex userid with seasoned user
* @param {User} user the user you want to lunk plex userid with
* @returns {Promsie}
*/
unlinkPlexUserId(username) {
return new Promise((resolve, reject) => {
this.database
.run(this.queries.unlink, username)
.then(row => resolve(row))
.catch(error => {
// TODO log this unknown db error
console.log("db error", error);
reject({
status: 500,
message:
"An unexpected error occured while unlinking plex and seasoned accounts",
source: "seasoned database"
});
});
});
}
/**
* Check if the user has boolean flag set for admin in database
* @param {User} user object
* @returns {Promsie}
*/
checkAdmin(user) {
return this.database
.get(this.queries.getAdminStateByUser, user.username)
.then(row => row.admin);
}
/**
* Get settings for user matching string username
* @param {String} username
* @returns {Promsie}
*/
getSettings(username) {
return new Promise((resolve, reject) => {
this.database
.get(this.queries.getSettings, username)
.then(async row => {
if (row == null) {
console.debug(
`settings do not exist for user: ${username}. Creating settings entry.`
);
const userExistsWithUsername = await this.database.get(
"select * from user where user_name is ?",
username
);
if (userExistsWithUsername !== undefined) {
try {
resolve(this.dbCreateSettings(username));
} catch (error) {
reject(error);
}
} else {
reject({
status: 404,
message: "User not found, no settings to get"
});
}
}
resolve(row);
})
.catch(error => {
console.error(
"Unexpected error occured while fetching settings for your account. Error:",
error
);
reject({
status: 500,
message:
"An unexpected error occured while fetching settings for your account",
source: "seasoned database"
});
});
});
}
/**
* Update settings values for user matching string username
* @param {String} username
* @param {String} dark_mode
* @param {String} emoji
* @returns {Promsie}
*/
updateSettings(username, dark_mode = undefined, emoji = undefined) {
const settings = this.getSettings(username);
dark_mode = dark_mode !== undefined ? dark_mode : settings.dark_mode;
emoji = emoji !== undefined ? emoji : settings.emoji;
return this.dbUpdateSettings(username, dark_mode, emoji).catch(error => {
if (error.status && error.message) {
return error;
}
return {
status: 500,
message:
"An unexpected error occured while updating settings for your account"
};
});
}
/**
* Helper function for creating settings in the database
* @param {String} username
* @returns {Promsie}
*/
dbCreateSettings(username) {
return this.database
.run(this.queries.createSettings, username)
.then(() => this.database.get(this.queries.getSettings, username))
.catch(error =>
rejectUnexpectedDatabaseError(
"Unexpected error occured while creating settings",
503,
error
)
);
}
/**
* Helper function for updating settings in the database
* @param {String} username
* @returns {Promsie}
*/
dbUpdateSettings(username, dark_mode, emoji) {
return new Promise((resolve, reject) =>
this.database
.run(this.queries.updateSettings, [username, dark_mode, emoji])
.then(row => resolve(row))
);
}
} }
const rejectUnexpectedDatabaseError = (
message,
status,
error,
reject = null
) => {
console.error(error);
const body = {
status,
message,
source: "seasoned database"
};
if (reject == null) {
return new Promise((resolve, reject) => reject(body));
}
reject(body);
};
module.exports = UserRepository; module.exports = UserRepository;

View File

@@ -1,73 +1,75 @@
const bcrypt = require('bcrypt-nodejs'); const bcrypt = require("bcrypt");
const UserRepository = require('src/user/userRepository'); const UserRepository = require("src/user/userRepository");
class UserSecurity { class UserSecurity {
constructor(database) { constructor(database) {
this.userRepository = new UserRepository(database); this.userRepository = new UserRepository(database);
} }
/** /**
* Create a new user in PlanFlix. * Create a new user in PlanFlix.
* @param {User} user the new user you want to create * @param {User} user the new user you want to create
* @param {String} clearPassword a password of the user * @param {String} clearPassword a password of the user
* @returns {Promise} * @returns {Promise}
*/ */
createNewUser(user, clearPassword) { createNewUser(user, clearPassword) {
if (user.username.trim() === '') { if (user.username.trim() === "") {
throw new Error('The username is empty.'); throw new Error("The username is empty.");
} else if (clearPassword.trim() === '') { } else if (clearPassword.trim() === "") {
throw new Error('The password is empty.'); throw new Error("The password is empty.");
} else { } else {
return Promise.resolve() return this.userRepository
.then(() => this.userRepository.create(user)) .create(user)
.then(() => UserSecurity.hashPassword(clearPassword)) .then(() => UserSecurity.hashPassword(clearPassword))
.then(hash => this.userRepository.changePassword(user, hash)); .then(hash => this.userRepository.changePassword(user, hash));
} }
} }
/** /**
* Login into PlanFlix. * Login into PlanFlix.
* @param {User} user the user you want to login * @param {User} user the user you want to login
* @param {String} clearPassword the user's password * @param {String} clearPassword the user's password
* @returns {Promise} * @returns {Promise}
*/ */
login(user, clearPassword) { login(user, clearPassword) {
return Promise.resolve() return this.userRepository
.then(() => this.userRepository.retrieveHash(user)) .retrieveHash(user)
.then(hash => UserSecurity.compareHashes(hash, clearPassword)) .then(hash => UserSecurity.compareHashes(hash, clearPassword))
.catch(() => { throw new Error('Wrong username or password.'); }); .catch(() => {
} throw new Error("Incorrect username or password.");
});
}
/** /**
* Compare between a password and a hash password from database. * Compare between a password and a hash password from database.
* @param {String} hash the hash password from database * @param {String} hash the hash password from database
* @param {String} clearPassword the user's password * @param {String} clearPassword the user's password
* @returns {Promise} * @returns {Promise}
*/ */
static compareHashes(hash, clearPassword) { static compareHashes(hash, clearPassword) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
bcrypt.compare(clearPassword, hash, (error, matches) => { bcrypt.compare(clearPassword, hash, (error, match) => {
if (matches === true) { if (match) resolve(true);
resolve(); reject(false);
} else {
reject();
}
});
}); });
} });
}
/** /**
* Hashes a password. * Hashes a password.
* @param {String} clearPassword the user's password * @param {String} clearPassword the user's password
* @returns {Promise} * @returns {Promise}
*/ */
static hashPassword(clearPassword) { static hashPassword(clearPassword) {
return new Promise((resolve) => { return new Promise(resolve => {
bcrypt.hash(clearPassword, null, null, (error, hash) => { const saltRounds = 10;
resolve(hash); bcrypt.hash(clearPassword, saltRounds, (error, hash) => {
}); if (error) reject(error);
resolve(hash);
}); });
} });
}
} }
module.exports = UserSecurity; module.exports = UserSecurity;

View File

@@ -1,137 +1,239 @@
const express = require('express'); const express = require("express");
const Raven = require('raven'); const Raven = require("raven");
const bodyParser = require('body-parser'); const cookieParser = require("cookie-parser");
const tokenToUser = require('./middleware/tokenToUser'); const bodyParser = require("body-parser");
const mustBeAuthenticated = require('./middleware/mustBeAuthenticated');
const mustBeAdmin = require('./middleware/mustBeAdmin');
const configuration = require('src/config/configuration').getInstance();
const listController = require('./controllers/list/listController'); const configuration = require("src/config/configuration").getInstance();
const reqTokenToUser = require("./middleware/reqTokenToUser");
const mustBeAuthenticated = require("./middleware/mustBeAuthenticated");
const mustBeAdmin = require("./middleware/mustBeAdmin");
const mustHaveAccountLinkedToPlex = require("./middleware/mustHaveAccountLinkedToPlex");
const listController = require("./controllers/list/listController");
const tautulli = require("./controllers/user/viewHistory.js");
const SettingsController = require("./controllers/user/settings");
const AuthenticatePlexAccountController = require("./controllers/user/authenticatePlexAccount");
// TODO: Have our raven router check if there is a value, if not don't enable raven. // TODO: Have our raven router check if there is a value, if not don't enable raven.
Raven.config(configuration.get('raven', 'DSN')).install(); Raven.config(configuration.get("raven", "DSN")).install();
const app = express(); // define our app using express const app = express(); // define our app using express
app.use(Raven.requestHandler()); app.use(Raven.requestHandler());
app.use(bodyParser.json()); app.use(bodyParser.json());
app.use(cookieParser());
const router = express.Router(); const router = express.Router();
const allowedOrigins = ['https://kevinmidboe.com', 'http://localhost:8080']; const allowedOrigins = configuration.get("webserver", "origins");
// TODO: All JSON handling in a single router // TODO: All JSON handling in a single router
// router.use(bodyParser.json()); // router.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true })); app.use(bodyParser.urlencoded({ extended: true }));
/* Decode the Authorization header if provided */ /* Check header and cookie for authentication and set req.loggedInUser */
router.use(tokenToUser); router.use(reqTokenToUser);
// TODO: Should have a separate middleware/router for handling headers. // TODO: Should have a separate middleware/router for handling headers.
router.use((req, res, next) => { router.use((req, res, next) => {
// TODO add logging of all incoming // TODO add logging of all incoming
console.log('Request: ', req.originalUrl); // const origin = req.headers.origin;
// if (allowedOrigins.indexOf(origin) > -1) {
// res.setHeader("Access-Control-Allow-Origin", origin);
// }
const origin = req.headers.origin; res.header(
if (allowedOrigins.indexOf(origin) > -1) { "Access-Control-Allow-Headers",
res.setHeader('Access-Control-Allow-Origin', origin); "Content-Type, Authorization, loggedinuser, set-cookie"
} );
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization, loggedinuser');
res.header('Access-Control-Allow-Methods', 'POST, GET, PUT');
next(); res.header("Access-Control-Allow-Credentials", "true");
res.header("Access-Control-Allow-Methods", "POST, GET, PUT, OPTIONS");
next();
}); });
router.get('/', function mainHandler(req, res) { router.get("/", (req, res) => {
throw new Error('Broke!'); res.send("welcome to seasoned api");
}); });
app.use(Raven.errorHandler()); app.use(Raven.errorHandler());
app.use(function onError(err, req, res, next) { app.use((err, req, res, next) => {
res.statusCode = 500; res.statusCode = 500;
res.end(res.sentry + '\n'); res.end(res.sentry + "\n");
}); });
/** /**
* User * User
*/ */
router.post('/v1/user', require('./controllers/user/register.js')); router.post("/v1/user", require("./controllers/user/register.js"));
router.post('/v1/user/login', require('./controllers/user/login.js')); router.post("/v1/user/login", require("./controllers/user/login.js"));
router.get('/v1/user/history', mustBeAuthenticated, require('./controllers/user/history.js')); router.post("/v1/user/logout", require("./controllers/user/logout.js"));
router.get('/v1/user/requests', mustBeAuthenticated, require('./controllers/user/requests.js')); router.get(
"/v1/user/settings",
mustBeAuthenticated,
SettingsController.getSettingsController
);
router.put(
"/v1/user/settings",
mustBeAuthenticated,
SettingsController.updateSettingsController
);
router.get(
"/v1/user/search_history",
mustBeAuthenticated,
require("./controllers/user/searchHistory.js")
);
router.get(
"/v1/user/requests",
mustBeAuthenticated,
require("./controllers/user/requests.js")
);
router.post(
"/v1/user/link_plex",
mustBeAuthenticated,
AuthenticatePlexAccountController.link
);
router.post(
"/v1/user/unlink_plex",
mustBeAuthenticated,
AuthenticatePlexAccountController.unlink
);
router.get(
"/v1/user/view_history",
mustHaveAccountLinkedToPlex,
tautulli.userViewHistoryController
);
router.get(
"/v1/user/watch_time",
mustHaveAccountLinkedToPlex,
tautulli.watchTimeStatsController
);
router.get(
"/v1/user/plays_by_day",
mustHaveAccountLinkedToPlex,
tautulli.getPlaysByDaysController
);
router.get(
"/v1/user/plays_by_dayofweek",
mustHaveAccountLinkedToPlex,
tautulli.getPlaysByDayOfWeekController
);
/** /**
* Seasoned * Seasoned
*/ */
router.get('/v1/seasoned/all', require('./controllers/seasoned/readStrays.js')); router.get("/v1/seasoned/all", require("./controllers/seasoned/readStrays.js"));
router.get('/v1/seasoned/:strayId', require('./controllers/seasoned/strayById.js')); router.get(
router.post('/v1/seasoned/verify/:strayId', require('./controllers/seasoned/verifyStray.js')); "/v1/seasoned/:strayId",
require("./controllers/seasoned/strayById.js")
);
router.post(
"/v1/seasoned/verify/:strayId",
require("./controllers/seasoned/verifyStray.js")
);
router.get('/v2/search/', require('./controllers/search/multiSearch.js')); router.get("/v2/search/", require("./controllers/search/multiSearch.js"));
router.get('/v2/search/movie', require('./controllers/search/movieSearch.js')); router.get("/v2/search/movie", require("./controllers/search/movieSearch.js"));
router.get('/v2/search/show', require('./controllers/search/showSearch.js')); router.get("/v2/search/show", require("./controllers/search/showSearch.js"));
router.get('/v2/search/person', require('./controllers/search/personSearch.js')); router.get(
"/v2/search/person",
require("./controllers/search/personSearch.js")
);
router.get('/v2/movie/now_playing', listController.nowPlayingMovies); router.get("/v2/movie/now_playing", listController.nowPlayingMovies);
router.get('/v2/movie/popular', listController.popularMovies); router.get("/v2/movie/popular", listController.popularMovies);
router.get('/v2/movie/top_rated', listController.topRatedMovies); router.get("/v2/movie/top_rated", listController.topRatedMovies);
router.get('/v2/movie/upcoming', listController.upcomingMovies); router.get("/v2/movie/upcoming", listController.upcomingMovies);
router.get("/v2/movie/:id/credits", require("./controllers/movie/credits.js"));
router.get(
"/v2/movie/:id/release_dates",
require("./controllers/movie/releaseDates.js")
);
router.get("/v2/movie/:id", require("./controllers/movie/info.js"));
router.get('/v2/show/now_playing', listController.nowPlayingShows); router.get("/v2/show/now_playing", listController.nowPlayingShows);
router.get('/v2/show/popular', listController.popularShows); router.get("/v2/show/popular", listController.popularShows);
router.get('/v2/show/top_rated', listController.topRatedShows); router.get("/v2/show/top_rated", listController.topRatedShows);
router.get("/v2/show/:id/credits", require("./controllers/show/credits.js"));
router.get("/v2/show/:id", require("./controllers/show/info.js"));
router.get(
"/v2/person/:id/credits",
require("./controllers/person/credits.js")
);
router.get("/v2/person/:id", require("./controllers/person/info.js"));
router.get('/v2/movie/:id', require('./controllers/info/movieInfo.js'));
router.get('/v2/show/:id', require('./controllers/info/showInfo.js'));
router.get('/v2/person/:id', require('./controllers/info/personInfo.js'));
/** /**
* Plex * Plex
*/ */
router.get('/v2/plex/search', require('./controllers/plex/search')); router.get("/v2/plex/search", require("./controllers/plex/search"));
/** /**
* List * List
*/ */
router.get('/v1/plex/search', require('./controllers/plex/searchMedia.js')); router.get("/v1/plex/search", require("./controllers/plex/searchMedia.js"));
router.get('/v1/plex/playing', require('./controllers/plex/plexPlaying.js')); router.get("/v1/plex/playing", require("./controllers/plex/plexPlaying.js"));
router.get('/v1/plex/request', require('./controllers/plex/searchRequest.js')); router.get("/v1/plex/request", require("./controllers/plex/searchRequest.js"));
router.get('/v1/plex/request/:mediaId', require('./controllers/plex/readRequest.js')); router.get(
router.post('/v1/plex/request/:mediaId', require('./controllers/plex/submitRequest.js')); "/v1/plex/request/:mediaId",
router.post('/v1/plex/hook', require('./controllers/plex/hookDump.js')); require("./controllers/plex/readRequest.js")
);
router.post(
"/v1/plex/request/:mediaId",
require("./controllers/plex/submitRequest.js")
);
router.post("/v1/plex/hook", require("./controllers/plex/hookDump.js"));
router.get(
"/v1/plex/watch-link",
mustBeAuthenticated,
require("./controllers/plex/watchDirectLink.js")
);
/** /**
* Requests * Requests
*/ */
router.get('/v2/request', require('./controllers/request/fetchAllRequests.js')); router.get("/v2/request", require("./controllers/request/fetchAllRequests.js"));
router.get('/v2/request/:id', require('./controllers/request/getRequest.js')); router.get("/v2/request/:id", require("./controllers/request/getRequest.js"));
router.post('/v2/request', require('./controllers/request/requestTmdbId.js')); router.post("/v2/request", require("./controllers/request/requestTmdbId.js"));
router.get('/v1/plex/requests/all', require('./controllers/plex/fetchRequested.js')); router.get(
router.put('/v1/plex/request/:requestId', mustBeAuthenticated, require('./controllers/plex/updateRequested.js')); "/v1/plex/requests/all",
require("./controllers/plex/fetchRequested.js")
);
router.put(
"/v1/plex/request/:requestId",
mustBeAuthenticated,
require("./controllers/plex/updateRequested.js")
);
/** /**
* Pirate * Pirate
*/ */
router.get('/v1/pirate/search', mustBeAuthenticated, require('./controllers/pirate/searchTheBay.js')); router.get(
router.post('/v1/pirate/add', mustBeAuthenticated, require('./controllers/pirate/addMagnet.js')); "/v1/pirate/search",
mustBeAuthenticated,
/** require("./controllers/pirate/searchTheBay.js")
* TMDB );
*/ router.post(
router.get('/v1/tmdb/search', require('./controllers/tmdb/searchMedia.js')); "/v1/pirate/add",
router.get('/v1/tmdb/list/:listname', require('./controllers/tmdb/listSearch.js')); mustBeAuthenticated,
router.get('/v1/tmdb/:mediaId', require('./controllers/tmdb/readMedia.js')); require("./controllers/pirate/addMagnet.js")
);
/** /**
* git * git
*/ */
router.post('/v1/git/dump', require('./controllers/git/dumpHook.js')); router.post("/v1/git/dump", require("./controllers/git/dumpHook.js"));
/** /**
* misc * misc
*/ */
router.get('/v1/emoji', require('./controllers/misc/emoji.js')); router.get("/v1/emoji", require("./controllers/misc/emoji.js"));
// REGISTER OUR ROUTES ------------------------------- // REGISTER OUR ROUTES -------------------------------
// all of our routes will be prefixed with /api // all of our routes will be prefixed with /api
app.use('/api', router); app.use("/api", router);
module.exports = app; module.exports = app;

View File

@@ -1,30 +0,0 @@
const configuration = require('src/config/configuration').getInstance();
const Cache = require('src/tmdb/cache');
const TMDB = require('src/tmdb/tmdb');
const Plex = require('src/plex/plex');
const cache = new Cache();
const tmdb = new TMDB(cache, configuration.get('tmdb', 'apiKey'));
const plex = new Plex(configuration.get('plex', 'ip'));
/**
* Controller: Retrieve information for a movie
* @param {Request} req http request variable
* @param {Response} res
* @returns {Callback}
*/
async function movieInfoController(req, res) {
const movieId = req.params.id;
const { credits } = req.query;
const movie = await tmdb.movieInfo(movieId, credits);
plex.existsInPlex(movie)
.catch((error) => { console.log('Error when searching plex'); })
.then(() => {
console.log('movie', movie)
res.send(movie);
}).catch((error) => {
res.status(404).send({ success: false, error: error.message });
});
}
module.exports = movieInfoController;

View File

@@ -1,24 +0,0 @@
const configuration = require('src/config/configuration').getInstance();
const Cache = require('src/tmdb/cache');
const TMDB = require('src/tmdb/tmdb');
const cache = new Cache();
const tmdb = new TMDB(cache, configuration.get('tmdb', 'apiKey'));
/**
* Controller: Retrieve information for a person
* @param {Request} req http request variable
* @param {Response} res
* @returns {Callback}
*/
function personInfoController(req, res) {
const personId = req.params.id;
tmdb.personInfo(personId)
.then((person) => {
res.send(person);
}).catch((error) => {
res.status(404).send({ success: false, error: error.message });
});
}
module.exports = personInfoController;

View File

@@ -1,31 +0,0 @@
const configuration = require('src/config/configuration').getInstance();
const Cache = require('src/tmdb/cache');
const TMDB = require('src/tmdb/tmdb');
const Plex = require('src/plex/plex');
const cache = new Cache();
const tmdb = new TMDB(cache, configuration.get('tmdb', 'apiKey'));
const plex = new Plex(configuration.get('plex', 'ip'));
/**
* Controller: Retrieve information for a show
* @param {Request} req http request variable
* @param {Response} res
* @returns {Callback}
*/
async function showInfoController(req, res) {
const showId = req.params.id;
const { credits } = req.query;
const show = await tmdb.showInfo(showId, credits);
plex.existsInPlex(show)
.catch((error) => { console.log('Error when searching plex'); })
.then(() => {
console.log('show', show)
res.send(show);
}).catch((error) => {
res.status(404).send({ success: false, error: error.message });
});
}
module.exports = showInfoController;

View File

@@ -1,8 +1,6 @@
const configuration = require('src/config/configuration').getInstance(); const configuration = require('src/config/configuration').getInstance();
const Cache = require('src/tmdb/cache');
const TMDB = require('src/tmdb/tmdb'); const TMDB = require('src/tmdb/tmdb');
const cache = new Cache(); const tmdb = new TMDB(configuration.get('tmdb', 'apiKey'));
const tmdb = new TMDB(cache, configuration.get('tmdb', 'apiKey'));
// there should be a translate function from query params to // there should be a translate function from query params to
// tmdb list that is valid. Should it be a helper function or does it // tmdb list that is valid. Should it be a helper function or does it
@@ -16,70 +14,55 @@ const tmdb = new TMDB(cache, configuration.get('tmdb', 'apiKey'));
// + newly created (tv/latest). // + newly created (tv/latest).
// + movie/latest // + movie/latest
// //
function handleError(error, res) {
const { status, message } = error;
function getTmdbMovieList(res, listname, page) { if (status && message) {
Promise.resolve() res.status(status).send({ success: false, message })
.then(() => tmdb.movieList(listname, page)) } else {
.then((response) => res.send(response)) console.log('caught list controller error', error)
.catch((error) => { res.status(500).send({ message: 'An unexpected error occured while requesting list'})
res.status(500).send({ success: false, error: error.message }); }
})
} }
function getTmdbShowList(res, listname, page) { function handleListResponse(response, res) {
Promise.resolve() return res.send(response)
.then(() => tmdb.showList(listname, page)) .catch(error => handleError(error, res))
.then((response) => res.send(response))
.catch((error) => {
res.status(500).send({ success: false, error: error.message });
})
} }
exports.nowPlayingMovies = (req, res) => { function fetchTmdbList(req, res, listname, type) {
const { page } = req.query; const { page } = req.query;
const listname = 'miscNowPlayingMovies'
getTmdbMovieList(res, listname, page); if (type === 'movie') {
return tmdb.movieList(listname, page)
.then(listResponse => res.send(listResponse))
.catch(error => handleError(error, res))
} else if (type === 'show') {
return tmdb.showList(listname, page)
.then(listResponse => res.send(listResponse))
.catch(error => handleError(error, res))
}
handleError({
status: 400,
message: `'${type}' is not a valid list type.`
}, res)
} }
exports.popularMovies = (req, res) => { const nowPlayingMovies = (req, res) => fetchTmdbList(req, res, 'miscNowPlayingMovies', 'movie')
const { page } = req.query; const popularMovies = (req, res) => fetchTmdbList(req, res, 'miscPopularMovies', 'movie')
const listname = 'miscPopularMovies' const topRatedMovies = (req, res) => fetchTmdbList(req, res, 'miscTopRatedMovies', 'movie')
const upcomingMovies = (req, res) => fetchTmdbList(req, res, 'miscUpcomingMovies', 'movie')
const nowPlayingShows = (req, res) => fetchTmdbList(req, res, 'tvOnTheAir', 'show')
const popularShows = (req, res) => fetchTmdbList(req, res, 'miscPopularTvs', 'show')
const topRatedShows = (req, res) => fetchTmdbList(req, res, 'miscTopRatedTvs', 'show')
getTmdbMovieList(res, listname, page); module.exports = {
} nowPlayingMovies,
popularMovies,
exports.topRatedMovies = (req, res) => { topRatedMovies,
const { page } = req.query; upcomingMovies,
const listname = 'miscTopRatedMovies' nowPlayingShows,
popularShows,
getTmdbMovieList(res, listname, page); topRatedShows
}
exports.upcomingMovies = (req, res) => {
const { page } = req.query;
const listname = 'miscUpcomingMovies'
getTmdbMovieList(res, listname, page);
}
exports.nowPlayingShows = (req, res) => {
const { page } = req.query;
const listname = 'tvOnTheAir'
getTmdbShowList(res, listname, page);
}
exports.popularShows = (req, res) => {
const { page } = req.query;
const listname = 'miscPopularTvs'
getTmdbShowList(res, listname, page);
}
exports.topRatedShows = (req, res) => {
const { page } = req.query;
const listname = 'miscTopRatedTvs'
getTmdbShowList(res, listname, page);
} }

View File

@@ -0,0 +1,24 @@
const configuration = require('src/config/configuration').getInstance();
const TMDB = require('src/tmdb/tmdb');
const tmdb = new TMDB(configuration.get('tmdb', 'apiKey'));
const movieCreditsController = (req, res) => {
const movieId = req.params.id;
tmdb.movieCredits(movieId)
.then(credits => res.send(credits.createJsonResponse()))
.catch(error => {
const { status, message } = error;
if (status && message) {
res.status(status).send({ success: false, message })
} else {
// TODO log unhandled errors
console.log('caugth movie credits controller error', error)
res.status(500).send({ message: 'An unexpected error occured while requesting movie credits' })
}
})
}
module.exports = movieCreditsController;

View File

@@ -0,0 +1,71 @@
const configuration = require("src/config/configuration").getInstance();
const TMDB = require("src/tmdb/tmdb");
const Plex = require("src/plex/plex");
const tmdb = new TMDB(configuration.get("tmdb", "apiKey"));
const plex = new Plex(configuration.get("plex", "ip"));
function handleError(error, res) {
const { status, message } = error;
if (status && message) {
res.status(status).send({ success: false, message });
} else {
console.log("caught movieinfo controller error", error);
res.status(500).send({
message: "An unexpected error occured while requesting movie info"
});
}
}
/**
* Controller: Retrieve information for a movie
* @param {Request} req http request variable
* @param {Response} res
* @returns {Callback}
*/
async function movieInfoController(req, res) {
const movieId = req.params.id;
let { credits, release_dates, check_existance } = req.query;
credits && credits.toLowerCase() === "true"
? (credits = true)
: (credits = false);
release_dates && release_dates.toLowerCase() === "true"
? (release_dates = true)
: (release_dates = false);
check_existance && check_existance.toLowerCase() === "true"
? (check_existance = true)
: (check_existance = false);
let tmdbQueue = [tmdb.movieInfo(movieId)];
if (credits) tmdbQueue.push(tmdb.movieCredits(movieId));
if (release_dates) tmdbQueue.push(tmdb.movieReleaseDates(movieId));
try {
const [Movie, Credits, ReleaseDates] = await Promise.all(tmdbQueue);
const movie = Movie.createJsonResponse();
if (Credits) movie.credits = Credits.createJsonResponse();
if (ReleaseDates)
movie.release_dates = ReleaseDates.createJsonResponse().results;
if (check_existance) {
try {
movie.exists_in_plex = await plex.existsInPlex(movie);
} catch (error) {
if (error.status === 401) {
console.log("Unathorized request, check plex server LAN settings");
} else {
console.log("Unkown error from plex!");
}
console.log(error);
}
}
res.send(movie);
} catch (error) {
handleError(error, res);
}
}
module.exports = movieInfoController;

View File

@@ -0,0 +1,24 @@
const configuration = require('src/config/configuration').getInstance();
const TMDB = require('src/tmdb/tmdb');
const tmdb = new TMDB(configuration.get('tmdb', 'apiKey'));
const movieReleaseDatesController = (req, res) => {
const movieId = req.params.id;
tmdb.movieReleaseDates(movieId)
.then(releaseDates => res.send(releaseDates.createJsonResponse()))
.catch(error => {
const { status, message } = error;
if (status && message) {
res.status(status).send({ success: false, message })
} else {
// TODO log unhandled errors : here our at tmdbReleaseError ?
console.log('caugth release dates controller error', error)
res.status(500).send({ message: 'An unexpected error occured while requesting movie credits' })
}
})
}
module.exports = movieReleaseDatesController;

View File

@@ -0,0 +1,26 @@
const configuration = require("src/config/configuration").getInstance();
const TMDB = require("src/tmdb/tmdb");
const tmdb = new TMDB(configuration.get("tmdb", "apiKey"));
const personCreditsController = (req, res) => {
const personId = req.params.id;
return tmdb
.personCredits(personId)
.then(credits => res.send(credits))
.catch(error => {
const { status, message } = error;
if (status && message) {
res.status(status).send({ success: false, message });
} else {
// TODO log unhandled errors
console.log("caugth show credits controller error", error);
res.status(500).send({
message: "An unexpected error occured while requesting person credits"
});
}
});
};
module.exports = personCreditsController;

View File

@@ -0,0 +1,49 @@
const configuration = require("src/config/configuration").getInstance();
const TMDB = require("src/tmdb/tmdb");
const tmdb = new TMDB(configuration.get("tmdb", "apiKey"));
function handleError(error, res) {
const { status, message } = error;
if (status && message) {
res.status(status).send({ success: false, message });
} else {
console.log("caught personinfo controller error", error);
res.status(500).send({
message: "An unexpected error occured while requesting person info."
});
}
}
/**
* Controller: Retrieve information for a person
* @param {Request} req http request variable
* @param {Response} res
* @returns {Callback}
*/
async function personInfoController(req, res) {
const personId = req.params.id;
let { credits } = req.query;
arguments;
credits && credits.toLowerCase() === "true"
? (credits = true)
: (credits = false);
let tmdbQueue = [tmdb.personInfo(personId)];
if (credits) tmdbQueue.push(tmdb.personCredits(personId));
try {
const [Person, Credits] = await Promise.all(tmdbQueue);
const person = Person.createJsonResponse();
if (credits) person.credits = Credits.createJsonResponse();
return res.send(person);
} catch (error) {
handleError(error, res);
}
}
module.exports = personInfoController;

View File

@@ -17,8 +17,8 @@ function addMagnet(req, res) {
.then((result) => { .then((result) => {
res.send(result); res.send(result);
}) })
.catch((error) => { .catch(error => {
res.status(500).send({ success: false, error: error.message }); res.status(500).send({ success: false, message: error.message });
}); });
} }

View File

@@ -1,11 +1,11 @@
/* /*
* @Author: KevinMidboe * @Author: KevinMidboe
* @Date: 2017-10-21 09:54:31 * @Date: 2017-10-21 09:54:31
* @Last Modified by: KevinMidboe * @Last Modified by: KevinMidboe
* @Last Modified time: 2018-02-26 19:56:32 * @Last Modified time: 2018-02-26 19:56:32
*/ */
const PirateRepository = require('src/pirate/pirateRepository'); const PirateRepository = require("src/pirate/pirateRepository");
// const pirateRepository = new PirateRepository(); // const pirateRepository = new PirateRepository();
/** /**
@@ -15,15 +15,15 @@ const PirateRepository = require('src/pirate/pirateRepository');
* @returns {Callback} * @returns {Callback}
*/ */
function updateRequested(req, res) { function updateRequested(req, res) {
const { query, page, type } = req.query; const { query, page, type } = req.query;
PirateRepository.SearchPiratebay(query, page, type) PirateRepository.SearchPiratebay(query, page, type)
.then((result) => { .then(result => {
res.send({ success: true, results: result }); res.send({ success: true, results: result });
}) })
.catch((error) => { .catch(error => {
res.status(401).send({ success: false, error: error.message }); res.status(401).send({ success: false, message: error.message });
}); });
} }
module.exports = updateRequested; module.exports = updateRequested;

View File

@@ -17,7 +17,7 @@ function fetchRequestedController(req, res) {
res.send({ success: true, results: requestedItems, total_results: requestedItems.length }); res.send({ success: true, results: requestedItems, total_results: requestedItems.length });
}) })
.catch((error) => { .catch((error) => {
res.status(401).send({ success: false, error: error.message }); res.status(401).send({ success: false, message: error.message });
}); });
} }

View File

@@ -5,11 +5,11 @@ const plexRepository = new PlexRepository(configuration.get('plex', 'ip'));
function playingController(req, res) { function playingController(req, res) {
plexRepository.nowPlaying() plexRepository.nowPlaying()
.then((movies) => { .then(movies => {
res.send(movies); res.send(movies);
}) })
.catch((error) => { .catch(error => {
res.status(500).send({ success: false, error: error.message }); res.status(500).send({ success: false, message: error.message });
}); });
} }

View File

@@ -12,10 +12,10 @@ function readRequestController(req, res) {
const mediaId = req.params.mediaId; const mediaId = req.params.mediaId;
const { type } = req.query; const { type } = req.query;
requestRepository.lookup(mediaId, type) requestRepository.lookup(mediaId, type)
.then((movies) => { .then(movies => {
res.send(movies); res.send(movies);
}).catch((error) => { }).catch(error => {
res.status(404).send({ success: false, error: error.message }); res.status(404).send({ success: false, message: error.message });
}); });
} }

View File

@@ -11,14 +11,14 @@ const plex = new Plex(configuration.get('plex', 'ip'));
function searchPlexController(req, res) { function searchPlexController(req, res) {
const { query, type } = req.query; const { query, type } = req.query;
plex.search(query, type) plex.search(query, type)
.then((movies) => { .then(movies => {
if (movies.length > 0) { if (movies.length > 0) {
res.send(movies); res.send(movies);
} else { } else {
res.status(404).send({ success: false, error: 'Search query did not give any results from plex.'}) res.status(404).send({ success: false, message: 'Search query did not give any results from plex.'})
} }
}).catch((error) => { }).catch(error => {
res.status(500).send({ success: false, error: error.message }); res.status(500).send({ success: false, message: error.message });
}); });
} }

View File

@@ -14,15 +14,15 @@ function searchMediaController(req, res) {
const { query } = req.query; const { query } = req.query;
plexRepository.search(query) plexRepository.search(query)
.then((media) => { .then(media => {
if (media !== undefined || media.length > 0) { if (media !== undefined || media.length > 0) {
res.send(media); res.send(media);
} else { } else {
res.status(404).send({ success: false, error: 'Search query did not return any results.' }); res.status(404).send({ success: false, message: 'Search query did not return any results.' });
} }
}) })
.catch((error) => { .catch(error => {
res.status(500).send({ success: false, error: error.message }); res.status(500).send({ success: false, message: error.message });
}); });
} }

View File

@@ -1,26 +1,24 @@
const SearchHistory = require('src/searchHistory/searchHistory'); const SearchHistory = require("src/searchHistory/searchHistory");
const Cache = require('src/tmdb/cache'); const Cache = require("src/tmdb/cache");
const RequestRepository = require('src/plex/requestRepository.js'); const RequestRepository = require("src/plex/requestRepository.js");
const cache = new Cache(); const cache = new Cache();
const requestRepository = new RequestRepository(cache); const requestRepository = new RequestRepository(cache);
const searchHistory = new SearchHistory(); const searchHistory = new SearchHistory();
function searchRequestController(req, res) { function searchRequestController(req, res) {
const user = req.loggedInUser; const { query, page, type } = req.query;
const { query, page, type } = req.query; const username = req.loggedInUser ? req.loggedInUser.username : null;
const username = user === undefined ? undefined : user.username;
Promise.resolve() Promise.resolve()
.then(() => searchHistory.create(username, query)) .then(() => searchHistory.create(username, query))
.then(() => requestRepository.search(query, page, type)) .then(() => requestRepository.search(query, page, type))
.then((searchResult) => { .then(searchResult => {
res.send(searchResult); res.send(searchResult);
}) })
.catch((error) => { .catch(error => {
res.status(500).send({ success: false, error: error }); res.status(500).send({ success: false, message: error.message });
}); });
} }
module.exports = searchRequestController; module.exports = searchRequestController;

View File

@@ -1,19 +1,16 @@
const configuration = require('src/config/configuration').getInstance() const configuration = require("src/config/configuration").getInstance();
const RequestRepository = require('src/request/request'); const RequestRepository = require("src/request/request");
const Cache = require('src/tmdb/cache') const TMDB = require("src/tmdb/tmdb");
const TMDB = require('src/tmdb/tmdb') const tmdb = new TMDB(configuration.get("tmdb", "apiKey"));
const request = new RequestRepository();
const cache = new Cache() const tmdbMovieInfo = id => {
const tmdb = new TMDB(cache, configuration.get('tmdb', 'apiKey')) return tmdb.movieInfo(id);
const request = new RequestRepository() };
const tmdbMovieInfo = (id) => { const tmdbShowInfo = id => {
return tmdb.movieInfo(id) return tmdb.showInfo(id);
} };
const tmdbShowInfo = (id) => {
return tmdb.showInfo(id)
}
/** /**
* Controller: POST a media id to be donwloaded * Controller: POST a media id to be donwloaded
@@ -24,28 +21,43 @@ const tmdbShowInfo = (id) => {
function submitRequestController(req, res) { function submitRequestController(req, res) {
// This is the id that is the param of the url // This is the id that is the param of the url
const id = req.params.mediaId; const id = req.params.mediaId;
const type = req.query.type ? req.query.type.toLowerCase() : undefined const type = req.query.type ? req.query.type.toLowerCase() : undefined;
const ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress; const ip = req.headers["x-forwarded-for"] || req.connection.remoteAddress;
const user_agent = req.headers['user-agent']; const user_agent = req.headers["user-agent"];
const user = req.loggedInUser; const username = req.loggedInUser ? req.loggedInUser.username : null;
let mediaFunction = undefined
if (type === 'movie') { let mediaFunction = undefined;
console.log('movie')
mediaFunction = tmdbMovieInfo if (type === "movie") {
} else if (type === 'show') { console.log("movie");
console.log('show') mediaFunction = tmdbMovieInfo;
mediaFunction = tmdbShowInfo } else if (type === "show") {
console.log("show");
mediaFunction = tmdbShowInfo;
} else { } else {
res.status(422).send({ success: false, error: 'Incorrect type. Allowed types: "movie" or "show"'}) res
.status(422)
.send({
success: false,
message: 'Incorrect type. Allowed types: "movie" or "show"'
});
} }
if (mediaFunction === undefined) { res.status(200); return } if (mediaFunction === undefined) {
res.status(200);
return;
}
mediaFunction(id) mediaFunction(id)
.then(tmdbMedia => request.requestFromTmdb(tmdbMedia, ip, user_agent, user)) .then(tmdbMedia =>
.then(() => res.send({ success: true, message: 'Media item successfully requested' })) request.requestFromTmdb(tmdbMedia, ip, user_agent, username)
.catch(err => res.status(500).send({ success: false, error: err.message })) )
.then(() =>
res.send({ success: true, message: "Media item successfully requested" })
)
.catch(err =>
res.status(500).send({ success: false, message: err.message })
);
} }
module.exports = submitRequestController; module.exports = submitRequestController;

View File

@@ -18,7 +18,7 @@ function updateRequested(req, res) {
res.send({ success: true }); res.send({ success: true });
}) })
.catch((error) => { .catch((error) => {
res.status(401).send({ success: false, error: error.message }); res.status(401).send({ success: false, message: error.message });
}); });
} }

View File

@@ -0,0 +1,27 @@
const configuration = require('src/config/configuration').getInstance();
const Plex = require('src/plex/plex');
const plex = new Plex(configuration.get('plex', 'ip'));
/**
* Controller: Search plex for movies, shows and episodes by query
* @param {Request} req http request variable
* @param {Response} res
* @returns {Callback}
*/
function watchDirectLink (req, res) {
const { title, year } = req.query;
plex.getDirectLinkByTitleAndYear(title, year)
.then(plexDirectLink => {
if (plexDirectLink == false)
res.status(404).send({ success: true, link: null })
else
res.status(200).send({ success: true, link: plexDirectLink })
})
.catch(error => {
res.status(500).send({ success: false, message: error.message });
});
}
module.exports = watchDirectLink;

View File

@@ -18,9 +18,9 @@ function fetchAllRequests(req, res) {
Promise.resolve() Promise.resolve()
.then(() => request.fetchAll(page, sort_by, sort_direction, filter, query)) .then(() => request.fetchAll(page, sort_by, sort_direction, filter, query))
.then((result) => res.send(result)) .then(result => res.send(result))
.catch((error) => { .catch(error => {
res.status(404).send({ success: false, error: error.message }); res.status(404).send({ success: false, message: error.message });
}); });
} }

View File

@@ -11,11 +11,10 @@ function fetchAllRequests(req, res) {
const id = req.params.id; const id = req.params.id;
const { type } = req.query; const { type } = req.query;
Promise.resolve() request.getRequestByIdAndType(id, type)
.then(() => request.getRequestByIdAndType(id, type)) .then(result => res.send(result))
.then((result) => res.send(result)) .catch(error => {
.catch((error) => { res.status(404).send({ success: false, message: error.message });
res.status(404).send({ success: false, error: error.message });
}); });
} }

View File

@@ -1,18 +1,17 @@
const configuration = require('src/config/configuration').getInstance(); const configuration = require("src/config/configuration").getInstance();
const Cache = require('src/tmdb/cache'); const TMDB = require("src/tmdb/tmdb");
const TMDB = require('src/tmdb/tmdb'); const RequestRepository = require("src/request/request");
const RequestRepository = require('src/request/request'); const tmdb = new TMDB(configuration.get("tmdb", "apiKey"));
const cache = new Cache();
const tmdb = new TMDB(cache, configuration.get('tmdb', 'apiKey'));
const request = new RequestRepository(); const request = new RequestRepository();
// const { sendSMS } = require("src/notifications/sms");
const tmdbMovieInfo = (id) => { const tmdbMovieInfo = id => {
return tmdb.movieInfo(id) return tmdb.movieInfo(id);
} };
const tmdbShowInfo = (id) => { const tmdbShowInfo = id => {
return tmdb.showInfo(id) return tmdb.showInfo(id);
} };
/** /**
* Controller: Request by id with type param * Controller: Request by id with type param
@@ -21,32 +20,48 @@ const tmdbShowInfo = (id) => {
* @returns {Callback} * @returns {Callback}
*/ */
function requestTmdbIdController(req, res) { function requestTmdbIdController(req, res) {
const { id, type } = req.body const { id, type } = req.body;
console.log('body', req.body)
console.log('id & type', id, type)
const ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress; const ip = req.headers["x-forwarded-for"] || req.connection.remoteAddress;
const user_agent = req.headers['user-agent']; const user_agent = req.headers["user-agent"];
const user = req.loggedInUser; const username = req.loggedInUser ? req.loggedInUser.username : null;
let mediaFunction = undefined
if (type === 'movie') { let mediaFunction = undefined;
console.log('movie')
mediaFunction = tmdbMovieInfo if (id === undefined || type === undefined) {
} else if (type === 'show') { res.status(422).send({
console.log('show') success: false,
mediaFunction = tmdbShowInfo message: "'Missing parameteres: 'id' and/or 'type'"
});
}
if (type === "movie") {
mediaFunction = tmdbMovieInfo;
} else if (type === "show") {
mediaFunction = tmdbShowInfo;
} else { } else {
res.status(422).send({ success: false, error: 'Incorrect type. Allowed types: "movie" or "show"'}) res.status(422).send({
success: false,
message: 'Incorrect type. Allowed types: "movie" or "show"'
});
} }
mediaFunction(id) mediaFunction(id)
.catch((error) => { console.error(error); res.status(404).send({ success: false, error: 'Id not found' }) }) // .catch((error) => { console.error(error); res.status(404).send({ success: false, error: 'Id not found' }) })
.then((tmdbMedia) => request.requestFromTmdb(tmdbMedia, ip, user_agent, user)) .then(tmdbMedia => {
.then(() => res.send({success: true, message: 'Request has been submitted.'})) request.requestFromTmdb(tmdbMedia, ip, user_agent, username);
.catch((error) => {
res.status(501).send({ success: false, error: error.message }); // TODO enable SMS
// const url = `https://request.movie?${tmdbMedia.type}=${tmdbMedia.id}`;
// const message = `${tmdbMedia.title} (${tmdbMedia.year}) requested!\n${url}`;
// sendSMS(message);
}) })
.then(() =>
res.send({ success: true, message: "Request has been submitted." })
)
.catch(error => {
res.send({ success: false, message: error.message });
});
} }
module.exports = requestTmdbIdController; module.exports = requestTmdbIdController;

View File

@@ -1,9 +1,7 @@
const SearchHistory = require('src/searchHistory/searchHistory'); const configuration = require("src/config/configuration").getInstance();
const configuration = require('src/config/configuration').getInstance(); const TMDB = require("src/tmdb/tmdb");
const Cache = require('src/tmdb/cache'); const SearchHistory = require("src/searchHistory/searchHistory");
const TMDB = require('src/tmdb/tmdb'); const tmdb = new TMDB(configuration.get("tmdb", "apiKey"));
const cache = new Cache();
const tmdb = new TMDB(cache, configuration.get('tmdb', 'apiKey'));
const searchHistory = new SearchHistory(); const searchHistory = new SearchHistory();
/** /**
@@ -13,23 +11,30 @@ const searchHistory = new SearchHistory();
* @returns {Callback} * @returns {Callback}
*/ */
function movieSearchController(req, res) { function movieSearchController(req, res) {
const user = req.loggedInUser; const { query, page, adult } = req.query;
const { query, page } = req.query; const username = req.loggedInUser ? req.loggedInUser.username : null;
const includeAdult = adult == "true" ? true : false;
Promise.resolve() if (username) {
.then(() => { searchHistory.create(username, query);
if (user) { }
return searchHistory.create(user, query);
} return tmdb
return null .movieSearch(query, page, includeAdult)
}) .then(movieSearchResults => res.send(movieSearchResults))
.then(() => tmdb.movieSearch(query, page)) .catch(error => {
.then((movies) => { const { status, message } = error;
res.send(movies);
}) if (status && message) {
.catch((error) => { res.status(status).send({ success: false, message });
res.status(500).send({ success: false, error: error.message }); } else {
}); // TODO log unhandled errors
console.log("caugth movie search controller error", error);
res.status(500).send({
message: `An unexpected error occured while searching movies with query: ${query}`
});
}
});
} }
module.exports = movieSearchController; module.exports = movieSearchController;

View File

@@ -1,11 +1,16 @@
const SearchHistory = require('src/searchHistory/searchHistory'); const configuration = require("src/config/configuration").getInstance();
const configuration = require('src/config/configuration').getInstance(); const TMDB = require("src/tmdb/tmdb");
const Cache = require('src/tmdb/cache'); const SearchHistory = require("src/searchHistory/searchHistory");
const TMDB = require('src/tmdb/tmdb'); const tmdb = new TMDB(configuration.get("tmdb", "apiKey"));
const cache = new Cache();
const tmdb = new TMDB(cache, configuration.get('tmdb', 'apiKey'));
const searchHistory = new SearchHistory(); const searchHistory = new SearchHistory();
function checkAndCreateJsonResponse(result) {
if (typeof result["createJsonResponse"] === "function") {
return result.createJsonResponse();
}
return result;
}
/** /**
* Controller: Search for multi (movies, shows and people by query and pagey * Controller: Search for multi (movies, shows and people by query and pagey
* @param {Request} req http request variable * @param {Request} req http request variable
@@ -13,23 +18,31 @@ const searchHistory = new SearchHistory();
* @returns {Callback} * @returns {Callback}
*/ */
function multiSearchController(req, res) { function multiSearchController(req, res) {
const user = req.loggedInUser; const { query, page, adult } = req.query;
const { query, page } = req.query; const username = req.loggedInUser ? req.loggedInUser.username : null;
Promise.resolve() if (username) {
.then(() => { searchHistory.create(username, query);
if (user) { }
return searchHistory.create(user, query);
} return tmdb
return null .multiSearch(query, page, adult)
}) .then(multiSearchResults => res.send(multiSearchResults))
.then(() => tmdb.multiSearch(query, page)) .catch(error => {
.then((result) => { const { status, message } = error;
res.send(result);
}) if (status && message) {
.catch((error) => { res.status(status).send({ success: false, message });
res.status(500).send({ success: false, error: error.message }); } else {
}); // TODO log unhandled errors
console.log("caugth multi search controller error", error);
res
.status(500)
.send({
message: `An unexpected error occured while searching with query: ${query}`
});
}
});
} }
module.exports = multiSearchController; module.exports = multiSearchController;

View File

@@ -1,9 +1,7 @@
const SearchHistory = require('src/searchHistory/searchHistory'); const configuration = require("src/config/configuration").getInstance();
const configuration = require('src/config/configuration').getInstance(); const TMDB = require("src/tmdb/tmdb");
const Cache = require('src/tmdb/cache'); const SearchHistory = require("src/searchHistory/searchHistory");
const TMDB = require('src/tmdb/tmdb'); const tmdb = new TMDB(configuration.get("tmdb", "apiKey"));
const cache = new Cache();
const tmdb = new TMDB(cache, configuration.get('tmdb', 'apiKey'));
const searchHistory = new SearchHistory(); const searchHistory = new SearchHistory();
/** /**
@@ -13,23 +11,30 @@ const searchHistory = new SearchHistory();
* @returns {Callback} * @returns {Callback}
*/ */
function personSearchController(req, res) { function personSearchController(req, res) {
const user = req.loggedInUser; const { query, page, adult } = req.query;
const { query, page } = req.query; const username = req.loggedInUser ? req.loggedInUser.username : null;
const includeAdult = adult == "true" ? true : false;
Promise.resolve() if (username) {
.then(() => { searchHistory.create(username, query);
if (user) { }
return searchHistory.create(user, query);
} return tmdb
return null .personSearch(query, page, includeAdult)
}) .then(persons => res.send(persons))
.then(() => tmdb.personSearch(query, page)) .catch(error => {
.then((person) => { const { status, message } = error;
res.send(person);
}) if (status && message) {
.catch((error) => { res.status(status).send({ success: false, message });
res.status(500).send({ success: false, error: error.message }); } else {
}); // TODO log unhandled errors
console.log("caugth person search controller error", error);
res.status(500).send({
message: `An unexpected error occured while searching people with query: ${query}`
});
}
});
} }
module.exports = personSearchController; module.exports = personSearchController;

View File

@@ -1,9 +1,7 @@
const SearchHistory = require('src/searchHistory/searchHistory'); const SearchHistory = require("src/searchHistory/searchHistory");
const configuration = require('src/config/configuration').getInstance(); const configuration = require("src/config/configuration").getInstance();
const Cache = require('src/tmdb/cache'); const TMDB = require("src/tmdb/tmdb");
const TMDB = require('src/tmdb/tmdb'); const tmdb = new TMDB(configuration.get("tmdb", "apiKey"));
const cache = new Cache();
const tmdb = new TMDB(cache, configuration.get('tmdb', 'apiKey'));
const searchHistory = new SearchHistory(); const searchHistory = new SearchHistory();
/** /**
@@ -13,23 +11,22 @@ const searchHistory = new SearchHistory();
* @returns {Callback} * @returns {Callback}
*/ */
function showSearchController(req, res) { function showSearchController(req, res) {
const user = req.loggedInUser; const { query, page, adult } = req.query;
const { query, page } = req.query; const username = req.loggedInUser ? req.loggedInUser.username : null;
const includeAdult = adult == "true" ? true : false;
Promise.resolve() if (username) {
.then(() => { searchHistory.create(username, query);
if (user) { }
return searchHistory.create(user, query);
} return tmdb
return null .showSearch(query, page, includeAdult)
}) .then(shows => {
.then(() => tmdb.showSearch(query, page)) res.send(shows);
.then((shows) => { })
res.send(shows); .catch(error => {
}) res.status(500).send({ success: false, message: error.message });
.catch((error) => { });
res.status(500).send({ success: false, error: error.message });
});
} }
module.exports = showSearchController; module.exports = showSearchController;

View File

@@ -10,7 +10,7 @@ function readStraysController(req, res) {
res.send(strays); res.send(strays);
}) })
.catch((error) => { .catch((error) => {
res.status(500).send({ success: false, error: error.message }); res.status(500).send({ success: false, message: error.message });
}); });
} }

View File

@@ -10,7 +10,7 @@ function strayByIdController(req, res) {
res.send(stray); res.send(stray);
}) })
.catch((error) => { .catch((error) => {
res.status(500).send({ success: false, error: error.message }); res.status(500).send({ success: false, message: error.message });
}); });
} }

View File

@@ -10,7 +10,7 @@ function verifyStrayController(req, res) {
res.send({ success: true, message: 'Episode verified' }); res.send({ success: true, message: 'Episode verified' });
}) })
.catch((error) => { .catch((error) => {
res.status(500).send({ success: false, error: error.message }); res.status(500).send({ success: false, message: error.message });
}); });
} }

View File

@@ -0,0 +1,23 @@
const configuration = require('src/config/configuration').getInstance();
const TMDB = require('src/tmdb/tmdb');
const tmdb = new TMDB(configuration.get('tmdb', 'apiKey'));
const showCreditsController = (req, res) => {
const showId = req.params.id;
tmdb.showCredits(showId)
.then(credits => res.send(credits.createJsonResponse()))
.catch(error => {
const { status, message } = error;
if (status && message) {
res.status(status).send({ success: false, message })
} else {
// TODO log unhandled errors
console.log('caugth show credits controller error', error)
res.status(500).send({ message: 'An unexpected error occured while requesting show credits' })
}
})
}
module.exports = showCreditsController;

View File

@@ -0,0 +1,54 @@
const configuration = require('src/config/configuration').getInstance();
const TMDB = require('src/tmdb/tmdb');
const Plex = require('src/plex/plex');
const tmdb = new TMDB(configuration.get('tmdb', 'apiKey'));
const plex = new Plex(configuration.get('plex', 'ip'));
function handleError(error, res) {
const { status, message } = error;
if (status && message) {
res.status(status).send({ success: false, message })
} else {
console.log('caught showinfo controller error', error)
res.status(500).send({
message: 'An unexpected error occured while requesting show info.'
})
}
}
/**
* Controller: Retrieve information for a show
* @param {Request} req http request variable
* @param {Response} res
* @returns {Callback}
*/
async function showInfoController(req, res) {
const showId = req.params.id;
let { credits, check_existance } = req.query;
credits && credits.toLowerCase() === 'true' ? credits = true : credits = false
check_existance && check_existance.toLowerCase() === 'true' ? check_existance = true : check_existance = false
let tmdbQueue = [tmdb.showInfo(showId)]
if (credits)
tmdbQueue.push(tmdb.showCredits(showId))
try {
const [Show, Credits] = await Promise.all(tmdbQueue)
const show = Show.createJsonResponse()
if (credits)
show.credits = Credits.createJsonResponse()
if (check_existance)
show.exists_in_plex = await plex.existsInPlex(show)
res.send(show)
} catch(error) {
handleError(error, res)
}
}
module.exports = showInfoController;

View File

@@ -1,25 +0,0 @@
const configuration = require('src/config/configuration').getInstance();
const Cache = require('src/tmdb/cache');
const TMDB = require('src/tmdb/tmdb');
const cache = new Cache();
const tmdb = new TMDB(cache, configuration.get('tmdb', 'apiKey'));
/**
* Controller: Retrieve nowplaying movies / now airing shows
* @param {Request} req http request variable
* @param {Response} res
* @returns {Callback}
*/
function listSearchController(req, res) {
const listname = req.params.listname;
const { type, page } = req.query;
tmdb.listSearch(listname, type, page)
.then((results) => {
res.send(results);
}).catch((error) => {
res.status(404).send({ success: false, error: error.message });
});
}
module.exports = listSearchController;

View File

@@ -1,25 +0,0 @@
const configuration = require('src/config/configuration').getInstance();
const Cache = require('src/tmdb/cache');
const TMDB = require('src/tmdb/tmdb');
const cache = new Cache();
const tmdb = new TMDB(cache, configuration.get('tmdb', 'apiKey'));
/**
* Controller: Retrieve information for a movie
* @param {Request} req http request variable
* @param {Response} res
* @returns {Callback}
*/
function readMediaController(req, res) {
const mediaId = req.params.mediaId;
const { type } = req.query;
tmdb.lookup(mediaId, type)
.then((movies) => {
res.send(movies);
}).catch((error) => {
res.status(404).send({ success: false, error: error.message });
});
}
module.exports = readMediaController;

View File

@@ -1,31 +0,0 @@
const configuration = require('src/config/configuration').getInstance();
const Cache = require('src/tmdb/cache');
const TMDB = require('src/tmdb/tmdb');
const cache = new Cache();
const tmdb = new TMDB(cache, configuration.get('tmdb', 'apiKey'));
/**
* Controller: Search for movies by query, page and optional type
* @param {Request} req http request variable
* @param {Response} res
* @returns {Callback}
*/
function searchMediaController(req, res) {
const { query, page, type } = req.query;
Promise.resolve()
.then(() => tmdb.search(query, page, type))
.then((movies) => {
if (movies !== undefined || movies.length > 0) {
res.send(movies);
} else {
res.status(404).send({ success: false, error: 'Search query did not return any results.' });
}
})
.catch((error) => {
res.status(500).send({ success: false, error: error.message });
});
}
module.exports = searchMediaController;

View File

@@ -0,0 +1,93 @@
const UserRepository = require("src/user/userRepository");
const userRepository = new UserRepository();
const fetch = require("node-fetch");
const FormData = require("form-data");
function handleError(error, res) {
let { status, message, source } = error;
if (status && message) {
if (status === 401) {
(message = "Unauthorized. Please check plex credentials."),
(source = "plex");
}
res.status(status).send({ success: false, message, source });
} else {
console.log("caught authenticate plex account controller error", error);
res.status(500).send({
message:
"An unexpected error occured while authenticating your account with plex",
source
});
}
}
function handleResponse(response) {
if (!response.ok) {
throw {
success: false,
status: response.status,
message: response.statusText
};
}
return response.json();
}
function plexAuthenticate(username, password) {
const url = "https://plex.tv/api/v2/users/signin";
const form = new FormData();
form.append("login", username);
form.append("password", password);
form.append("rememberMe", "false");
const headers = {
Accept: "application/json, text/plain, */*",
"Content-Type": form.getHeaders()["content-type"],
"X-Plex-Client-Identifier": "seasonedRequest"
};
const options = {
method: "POST",
headers,
body: form
};
return fetch(url, options).then(resp => handleResponse(resp));
}
function link(req, res) {
const user = req.loggedInUser;
const { username, password } = req.body;
return plexAuthenticate(username, password)
.then(plexUser => userRepository.linkPlexUserId(user.username, plexUser.id))
.then(response =>
res.send({
success: true,
message:
"Successfully authenticated and linked plex account with seasoned request."
})
)
.catch(error => handleError(error, res));
}
function unlink(req, res) {
const username = req.loggedInUser ? req.loggedInUser.username : null;
return userRepository
.unlinkPlexUserId(username)
.then(response =>
res.send({
success: true,
message: "Successfully unlinked plex account from seasoned request."
})
)
.catch(error => handleError(error, res));
}
module.exports = {
link,
unlink
};

View File

@@ -1,24 +0,0 @@
const SearchHistory = require('src/searchHistory/searchHistory');
const searchHistory = new SearchHistory();
/**
* Controller: Retrieves search history of a logged in user
* @param {Request} req http request variable
* @param {Response} res
* @returns {Callback}
*/
function historyController(req, res) {
const user = req.loggedInUser;
const username = user === undefined ? undefined : user.username;
searchHistory.read(username)
.then((searchQueries) => {
res.send({ success: true, searchQueries });
})
.catch((error) => {
res.status(404).send({ success: false, error: error });
});
}
module.exports = historyController;

View File

@@ -1,33 +1,61 @@
const User = require('src/user/user'); const User = require("src/user/user");
const Token = require('src/user/token'); const Token = require("src/user/token");
const UserSecurity = require('src/user/userSecurity'); const UserSecurity = require("src/user/userSecurity");
const UserRepository = require('src/user/userRepository'); const UserRepository = require("src/user/userRepository");
const configuration = require('src/config/configuration').getInstance(); const configuration = require("src/config/configuration").getInstance();
const secret = configuration.get('authentication', 'secret'); const secret = configuration.get("authentication", "secret");
const userSecurity = new UserSecurity(); const userSecurity = new UserSecurity();
const userRepository = new UserRepository(); const userRepository = new UserRepository();
// TODO look to move some of the token generation out of the reach of the final "catch-all"
// catch including the, maybe sensitive, error message.
const isProduction = process.env.NODE_ENV === "production";
const cookieOptions = {
httpOnly: false,
secure: isProduction,
maxAge: 90 * 24 * 3600000, // 90 days
sameSite: isProduction ? "Strict" : "Lax"
};
/** /**
* Controller: Log in a user provided correct credentials. * Controller: Log in a user provided correct credentials.
* @param {Request} req http request variable * @param {Request} req http request variable
* @param {Response} res * @param {Response} res
* @returns {Callback} * @returns {Callback}
*/ */
function loginController(req, res) { async function loginController(req, res) {
const user = new User(req.body.username); const user = new User(req.body.username);
const password = req.body.password; const password = req.body.password;
userSecurity.login(user, password) try {
.then(() => userRepository.checkAdmin(user)) const [loggedIn, isAdmin, settings] = await Promise.all([
.then((checkAdmin) => { userSecurity.login(user, password),
const token = new Token(user).toString(secret); userRepository.checkAdmin(user),
const admin_state = checkAdmin === 1 ? true : false; userRepository.getSettings(user.username)
res.send({ success: true, token, admin: admin_state }); ]);
})
.catch((error) => { if (!loggedIn) {
res.status(401).send({ success: false, error: error.message }); return res.status(503).send({
success: false,
message: "Unexpected error! Unable to create user."
}); });
}
const token = new Token(
user,
isAdmin === 1 ? true : false,
settings
).toString(secret);
return res.cookie("authorization", token, cookieOptions).status(200).send({
success: true,
message: "Welcome to request.movie!"
});
} catch (error) {
return res.status(401).send({ success: false, message: error.message });
}
} }
module.exports = loginController; module.exports = loginController;

View File

@@ -0,0 +1,16 @@
/**
* Controller: Log out a user (destroy authorization token)
* @param {Request} req http request variable
* @param {Response} res
* @returns {Callback}
*/
async function logoutController(req, res) {
res.clearCookie("authorization");
return res.status(200).send({
success: true,
message: "Logged out, see you later!"
});
}
module.exports = logoutController;

View File

@@ -1,13 +1,21 @@
const User = require('src/user/user'); const User = require("src/user/user");
const Token = require('src/user/token'); const Token = require("src/user/token");
const UserSecurity = require('src/user/userSecurity'); const UserSecurity = require("src/user/userSecurity");
const UserRepository = require('src/user/userRepository'); const UserRepository = require("src/user/userRepository");
const configuration = require('src/config/configuration').getInstance(); const configuration = require("src/config/configuration").getInstance();
const secret = configuration.get('authentication', 'secret'); const secret = configuration.get("authentication", "secret");
const userSecurity = new UserSecurity(); const userSecurity = new UserSecurity();
const userRepository = new UserRepository(); const userRepository = new UserRepository();
const isProduction = process.env.NODE_ENV === "production";
const cookieOptions = {
httpOnly: false,
secure: isProduction,
maxAge: 90 * 24 * 3600000, // 90 days
sameSite: isProduction ? "Strict" : "Lax"
};
/** /**
* Controller: Register a new user * Controller: Register a new user
* @param {Request} req http request variable * @param {Request} req http request variable
@@ -15,21 +23,25 @@ const userRepository = new UserRepository();
* @returns {Callback} * @returns {Callback}
*/ */
function registerController(req, res) { function registerController(req, res) {
const user = new User(req.body.username, req.body.email); const user = new User(req.body.username, req.body.email);
const password = req.body.password; const password = req.body.password;
userSecurity.createNewUser(user, password) userSecurity
.then(() => userRepository.checkAdmin(user)) .createNewUser(user, password)
.then((checkAdmin) => { .then(() => {
const token = new Token(user).toString(secret); const token = new Token(user, false).toString(secret);
const admin_state = checkAdmin === 1 ? true : false;
res.send({ return res
success: true, message: 'Welcome to Seasoned!', token, admin: admin_state, .cookie("authorization", token, cookieOptions)
}); .status(200)
}) .send({
.catch((error) => { success: true,
res.status(401).send({ success: false, error: error.message }); message: "Welcome to Seasoned!"
}); });
})
.catch(error => {
res.status(401).send({ success: false, message: error.message });
});
} }
module.exports = registerController; module.exports = registerController;

View File

@@ -1,4 +1,4 @@
const RequestRepository = require('src/plex/requestRepository.js'); const RequestRepository = require("src/plex/requestRepository.js");
const requestRepository = new RequestRepository(); const requestRepository = new RequestRepository();
@@ -9,16 +9,20 @@ const requestRepository = new RequestRepository();
* @returns {Callback} * @returns {Callback}
*/ */
function requestsController(req, res) { function requestsController(req, res) {
const user = req.loggedInUser; const username = req.loggedInUser ? req.loggedInUser.username : null;
requestRepository.userRequests(user) requestRepository
.then((requests) => { .userRequests(username)
res.send({ success: true, results: requests, total_results: requests.length }); .then(requests => {
}) res.send({
.catch((error) => { success: true,
console.log(error) results: requests,
res.status(500).send({ success: false, error: error }); total_results: requests.length
}); });
})
.catch(error => {
res.status(500).send({ success: false, message: error.message });
});
} }
module.exports = requestsController; module.exports = requestsController;

View File

@@ -0,0 +1,24 @@
const SearchHistory = require("src/searchHistory/searchHistory");
const searchHistory = new SearchHistory();
/**
* Controller: Retrieves search history of a logged in user
* @param {Request} req http request variable
* @param {Response} res
* @returns {Callback}
*/
function historyController(req, res) {
const username = req.loggedInUser ? req.loggedInUser.username : null;
searchHistory
.read(username)
.then(searchQueries => {
res.send({ success: true, searchQueries });
})
.catch(error => {
res.status(404).send({ success: false, message: error.message });
});
}
module.exports = historyController;

View File

@@ -0,0 +1,41 @@
const UserRepository = require("src/user/userRepository");
const userRepository = new UserRepository();
/**
* Controller: Retrieves settings of a logged in user
* @param {Request} req http request variable
* @param {Response} res
* @returns {Callback}
*/
const getSettingsController = (req, res) => {
const username = req.loggedInUser ? req.loggedInUser.username : null;
userRepository
.getSettings(username)
.then(settings => {
res.send({ success: true, settings });
})
.catch(error => {
res.status(404).send({ success: false, message: error.message });
});
};
const updateSettingsController = (req, res) => {
const username = req.loggedInUser ? req.loggedInUser.username : null;
const idempotencyKey = req.headers("Idempotency-Key"); // TODO implement better transactions
const { dark_mode, emoji } = req.body;
userRepository
.updateSettings(username, dark_mode, emoji)
.then(settings => {
res.send({ success: true, settings });
})
.catch(error => {
res.status(404).send({ success: false, message: error.message });
});
};
module.exports = {
getSettingsController,
updateSettingsController
};

View File

@@ -0,0 +1,107 @@
const configuration = require("src/config/configuration").getInstance();
const Tautulli = require("src/tautulli/tautulli");
const apiKey = configuration.get("tautulli", "apiKey");
const ip = configuration.get("tautulli", "ip");
const port = configuration.get("tautulli", "port");
const tautulli = new Tautulli(apiKey, ip, port);
function handleError(error, res) {
const { status, message } = error;
if (status && message) {
return res.status(status).send({ success: false, message });
} else {
console.log("caught view history controller error", error);
return res.status(500).send({
message: "An unexpected error occured while fetching view history"
});
}
}
function watchTimeStatsController(req, res) {
const user = req.loggedInUser;
return tautulli
.watchTimeStats(user.plex_userid)
.then(data => {
return res.send({
success: true,
data: data.response.data,
message: "watch time successfully fetched from tautulli"
});
})
.catch(error => handleError(error, res));
}
function getPlaysByDayOfWeekController(req, res) {
const user = req.loggedInUser;
const { days, y_axis } = req.query;
return tautulli
.getPlaysByDayOfWeek(user.plex_userid, days, y_axis)
.then(data =>
res.send({
success: true,
data: data.response.data,
message: "play by day of week successfully fetched from tautulli"
})
)
.catch(error => handleError(error, res));
}
function getPlaysByDaysController(req, res) {
const user = req.loggedInUser;
const { days, y_axis } = req.query;
if (days === undefined) {
return res.status(422).send({
success: false,
message: "Missing parameter: days (number)"
});
}
const allowedYAxisDataType = ["plays", "duration"];
if (!allowedYAxisDataType.includes(y_axis)) {
return res.status(422).send({
success: false,
message: `Y axis parameter must be one of values: [${allowedYAxisDataType}]`
});
}
return tautulli
.getPlaysByDays(user.plex_userid, days, y_axis)
.then(data =>
res.send({
success: true,
data: data.response.data
})
)
.catch(error => handleError(error, res));
}
function userViewHistoryController(req, res) {
const user = req.loggedInUser;
// TODO here we should check if we can init tau
// and then return 501 Not implemented
return tautulli
.viewHistory(user.plex_userid)
.then(data => {
return res.send({
success: true,
data: data.response.data.data,
message: "view history successfully fetched from tautulli"
});
})
.catch(error => handleError(error, res));
// const username = user.username;
}
module.exports = {
watchTimeStatsController,
getPlaysByDaysController,
getPlaysByDayOfWeekController,
userViewHistoryController
};

View File

@@ -6,7 +6,7 @@ const mustBeAdmin = (req, res, next) => {
if (req.loggedInUser === undefined) { if (req.loggedInUser === undefined) {
return res.status(401).send({ return res.status(401).send({
success: false, success: false,
error: 'You must be logged in.', message: 'You must be logged in.',
}); });
} else { } else {
database.get(`SELECT admin FROM user WHERE user_name IS ?`, req.loggedInUser.username) database.get(`SELECT admin FROM user WHERE user_name IS ?`, req.loggedInUser.username)
@@ -15,7 +15,7 @@ const mustBeAdmin = (req, res, next) => {
if (isAdmin.admin == 0) { if (isAdmin.admin == 0) {
return res.status(401).send({ return res.status(401).send({
success: false, success: false,
error: 'You must be logged in as a admin.' message: 'You must be logged in as a admin.'
}) })
} }
}) })

View File

@@ -1,11 +1,11 @@
const mustBeAuthenticated = (req, res, next) => { const mustBeAuthenticated = (req, res, next) => {
if (req.loggedInUser === undefined) { if (req.loggedInUser === undefined) {
return res.status(401).send({ return res.status(401).send({
success: false, success: false,
error: 'You must be logged in.', message: "You must be logged in."
}); });
} }
return next(); return next();
}; };
module.exports = mustBeAuthenticated; module.exports = mustBeAuthenticated;

View File

@@ -0,0 +1,31 @@
const establishedDatabase = require('src/database/database');
const mustHaveAccountLinkedToPlex = (req, res, next) => {
let database = establishedDatabase;
const loggedInUser = req.loggedInUser;
if (loggedInUser === undefined) {
return res.status(401).send({
success: false,
message: 'You must have your account linked to a plex account.',
});
} else {
database.get(`SELECT plex_userid FROM settings WHERE user_name IS ?`, loggedInUser.username)
.then(row => {
const plex_userid = row.plex_userid;
if (plex_userid === null || plex_userid === undefined) {
return res.status(403).send({
success: false,
message: 'No plex account user id found for your user. Please authenticate your plex account at /user/authenticate.'
})
} else {
req.loggedInUser.plex_userid = plex_userid;
return next();
}
})
}
};
module.exports = mustHaveAccountLinkedToPlex;

View File

@@ -0,0 +1,32 @@
/* eslint-disable no-param-reassign */
const configuration = require("src/config/configuration").getInstance();
const Token = require("src/user/token");
const secret = configuration.get("authentication", "secret");
// Token example:
// curl -i -H "Authorization:[token]" localhost:31459/api/v1/user/history
const reqTokenToUser = (req, res, next) => {
const cookieAuthToken = req.cookies.authorization;
const headerAuthToken = req.headers.authorization;
if (cookieAuthToken || headerAuthToken) {
try {
const token = Token.fromString(
cookieAuthToken || headerAuthToken,
secret
);
req.loggedInUser = token.user;
} catch (error) {
req.loggedInUser = undefined;
}
} else {
// guest session
console.debug("No auth token in header or cookie.");
}
next();
};
module.exports = reqTokenToUser;

View File

@@ -1,23 +0,0 @@
/* eslint-disable no-param-reassign */
const configuration = require('src/config/configuration').getInstance();
const secret = configuration.get('authentication', 'secret');
const Token = require('src/user/token');
// Token example:
// curl -i -H "Authorization:[token]" localhost:31459/api/v1/user/history
const tokenToUser = (req, res, next) => {
const rawToken = req.headers.authorization;
if (rawToken) {
try {
const token = Token.fromString(rawToken, secret);
req.loggedInUser = token.user;
} catch (error) {
req.loggedInUser = undefined;
}
}
next();
};
module.exports = tokenToUser;

View File

@@ -1 +1 @@
{"adult":false,"backdrop_path":"/mVr0UiqyltcfqxbAUcLl9zWL8ah.jpg","belongs_to_collection":{"id":422837,"name":"Blade Runner Collection","poster_path":"/cWESb1o9lW2i2Z3Xllv9u40aNIk.jpg","backdrop_path":"/bSHZIvLoPBWyGLeiAudN1mXdvQX.jpg"},"budget":150000000,"genres":[{"id":9648,"name":"Mystery"},{"id":878,"name":"Science Fiction"},{"id":53,"name":"Thriller"}],"homepage":"http://bladerunnermovie.com/","id":335984,"imdb_id":"tt1856101","original_language":"en","original_title":"Blade Runner 2049","overview":"Thirty years after the events of the first film, a new blade runner, LAPD Officer K, unearths a long-buried secret that has the potential to plunge what's left of society into chaos. K's discovery leads him on a quest to find Rick Deckard, a former LAPD blade runner who has been missing for 30 years.","popularity":30.03,"poster_path":"/gajva2L0rPYkEWjzgFlBXCAVBE5.jpg","production_companies":[{"id":79529,"logo_path":"/gVN3k8emmKy4iV4KREWcCtxusZK.png","name":"Torridon Films","origin_country":"US"},{"id":101829,"logo_path":"/8IOjCvgjq0zTrtP91cWD3kL2jMK.png","name":"16:14 Entertainment","origin_country":"US"},{"id":1645,"logo_path":"/6Ry6uNBaa0IbbSs1XYIgX5DkA9r.png","name":"Scott Free Productions","origin_country":""},{"id":5,"logo_path":"/71BqEFAF4V3qjjMPCpLuyJFB9A.png","name":"Columbia Pictures","origin_country":"US"},{"id":1088,"logo_path":"/9WOE5AQUXbOtLU6GTwfjS8OMF0v.png","name":"Alcon Entertainment","origin_country":"US"},{"id":78028,"logo_path":"/sTFcDFfJaSVT3sv3DoaZDE4SlGB.png","name":"Thunderbird Entertainment","origin_country":"CA"},{"id":174,"logo_path":"/ky0xOc5OrhzkZ1N6KyUxacfQsCk.png","name":"Warner Bros. Pictures","origin_country":"US"}],"production_countries":[{"iso_3166_1":"CA","name":"Canada"},{"iso_3166_1":"US","name":"United States of America"},{"iso_3166_1":"HU","name":"Hungary"},{"iso_3166_1":"GB","name":"United Kingdom"}],"release_date":"2017-10-04","revenue":259239658,"runtime":163,"spoken_languages":[{"iso_639_1":"en","name":"English"},{"iso_639_1":"fi","name":"suomi"}],"status":"Released","tagline":"There's still a page left.","title":"Blade Runner 2049","video":false,"vote_average":7.3,"vote_count":5478} [{"adult":false,"backdrop_path":"/mVr0UiqyltcfqxbAUcLl9zWL8ah.jpg","belongs_to_collection":{"id":422837,"name":"Blade Runner Collection","poster_path":"/cWESb1o9lW2i2Z3Xllv9u40aNIk.jpg","backdrop_path":"/bSHZIvLoPBWyGLeiAudN1mXdvQX.jpg"},"budget":150000000,"genres":[{"id":9648,"name":"Mystery"},{"id":878,"name":"Science Fiction"},{"id":53,"name":"Thriller"}],"homepage":"http://bladerunnermovie.com/","id":335984,"imdb_id":"tt1856101","original_language":"en","original_title":"Blade Runner 2049","overview":"Thirty years after the events of the first film, a new blade runner, LAPD Officer K, unearths a long-buried secret that has the potential to plunge what's left of society into chaos. K's discovery leads him on a quest to find Rick Deckard, a former LAPD blade runner who has been missing for 30 years.","popularity":30.03,"poster_path":"/gajva2L0rPYkEWjzgFlBXCAVBE5.jpg","production_companies":[{"id":79529,"logo_path":"/gVN3k8emmKy4iV4KREWcCtxusZK.png","name":"Torridon Films","origin_country":"US"},{"id":101829,"logo_path":"/8IOjCvgjq0zTrtP91cWD3kL2jMK.png","name":"16:14 Entertainment","origin_country":"US"},{"id":1645,"logo_path":"/6Ry6uNBaa0IbbSs1XYIgX5DkA9r.png","name":"Scott Free Productions","origin_country":""},{"id":5,"logo_path":"/71BqEFAF4V3qjjMPCpLuyJFB9A.png","name":"Columbia Pictures","origin_country":"US"},{"id":1088,"logo_path":"/9WOE5AQUXbOtLU6GTwfjS8OMF0v.png","name":"Alcon Entertainment","origin_country":"US"},{"id":78028,"logo_path":"/sTFcDFfJaSVT3sv3DoaZDE4SlGB.png","name":"Thunderbird Entertainment","origin_country":"CA"},{"id":174,"logo_path":"/ky0xOc5OrhzkZ1N6KyUxacfQsCk.png","name":"Warner Bros. Pictures","origin_country":"US"}],"production_countries":[{"iso_3166_1":"CA","name":"Canada"},{"iso_3166_1":"US","name":"United States of America"},{"iso_3166_1":"HU","name":"Hungary"},{"iso_3166_1":"GB","name":"United Kingdom"}],"release_date":"2017-10-04","revenue":259239658,"runtime":163,"spoken_languages":[{"iso_639_1":"en","name":"English"},{"iso_639_1":"fi","name":"suomi"}],"status":"Released","tagline":"There's still a page left.","title":"Blade Runner 2049","video":false,"vote_average":7.3,"vote_count":5478}]

View File

@@ -0,0 +1,6 @@
{
"page":1,
"results":[],
"total_results":0,
"total_pages":1
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,16 @@
const tmdbMock = () => ({
error: null,
response: null,
searchMovie(query, callback) {
callback(this.error, this.response);
},
movieInfo(query, callback) {
callback(this.error, this.response);
},
miscPopularMovies(callback) {
console.log('miscPopMovies callback', callback)
callback(this.error, this.response);
},
});
module.exports = tmdbMock;

View File

@@ -5,8 +5,8 @@ xdescribe('As a developer I want the server to start', () => {
beforeEach(() => beforeEach(() =>
this.server = require('src/webserver/server')); this.server = require('src/webserver/server'));
it('should listen on port 31459', (done) => { it('should listen on port 31400', (done) => {
net.createConnection(31459, done); net.createConnection(31400, done);
}); });
afterEach(() => afterEach(() =>

View File

@@ -15,6 +15,6 @@ describe('As a user I want error when registering existing username', () => {
.post('/api/v1/user') .post('/api/v1/user')
.send({ username: 'test_user', password: 'password' }) .send({ username: 'test_user', password: 'password' })
.expect(401) .expect(401)
.then(response => assert.equal(response.text, '{"success":false,"error":"That username is already registered"}')) .then(response => assert.equal(response.text, '{"success":false,"message":"That username is already registered"}'))
); );
}); });

View File

@@ -7,17 +7,17 @@ const createToken = require('test/helpers/createToken');
const infoMovieSuccess = require('test/fixtures/blade_runner_2049-info-success-response.json'); const infoMovieSuccess = require('test/fixtures/blade_runner_2049-info-success-response.json');
describe('As a user I want to request a movie', () => { describe('As a user I want to request a movie', () => {
before(() => { before(async () => {
return resetDatabase() await resetDatabase()
.then(() => createUser('test_user', 'test@gmail.com', 'password')); await createUser('test_user', 'test@gmail.com', 'password')
}) })
before(() => createCacheEntry('mi:335984', infoMovieSuccess)); before(() => createCacheEntry('mi:335984:false', infoMovieSuccess));
it('should return 200 when item is requested', () => it('should return 200 when item is requested', () =>
request(app) request(app)
.post('/api/v2/request') .post('/api/v2/request')
.set('authorization', createToken('test_user', 'secret'))
.send({ id: 335984, type: 'movie' }) .send({ id: 335984, type: 'movie' })
.set('Authorization', createToken('test_user', 'secret'))
.expect(200) .expect(200)
); );
}); });

View File

@@ -1,28 +1,29 @@
const assert = require('assert'); const assert = require('assert');
const convertTmdbToMovie = require('src/tmdb/convertTmdbToMovie'); // const convertTmdbToMovie = require('src/tmdb/convertTmdbToMovie');
const { Movie } = require('src/tmdb/types');
const bladeRunnerQuerySuccess = require('test/fixtures/blade_runner_2049-info-success-response.json') const bladeRunnerQuerySuccess = require('test/fixtures/blade_runner_2049-info-success-response.json')
describe('Convert tmdb movieInfo to movie', () => { describe('Convert tmdb movieInfo to movie', () => {
beforeEach(() => this.bladeRunnerTmdbMovie = bladeRunnerQuerySuccess); beforeEach(() => [this.bladeRunnerTmdbMovie] = bladeRunnerQuerySuccess);
it('should translate the tmdb release date to movie year', () => { it('should translate the tmdb release date to movie year', () => {
const bladeRunner = convertTmdbToMovie(this.bladeRunnerTmdbMovie); const bladeRunner = Movie.convertFromTmdbResponse(this.bladeRunnerTmdbMovie);
assert.strictEqual(bladeRunner.year, 2017); assert.strictEqual(bladeRunner.year, 2017);
}); });
it('should translate the tmdb release date to instance of Date', () => { it('should translate the tmdb release date to instance of Date', () => {
const bladeRunner = convertTmdbToMovie(this.bladeRunnerTmdbMovie); const bladeRunner = Movie.convertFromTmdbResponse(this.bladeRunnerTmdbMovie);
assert(bladeRunner.release_date instanceof Date); assert(bladeRunner.releaseDate instanceof Date);
}); });
it('should translate the tmdb title to title', () => { it('should translate the tmdb title to title', () => {
const bladeRunner = convertTmdbToMovie(this.bladeRunnerTmdbMovie); const bladeRunner = Movie.convertFromTmdbResponse(this.bladeRunnerTmdbMovie);
assert.equal(bladeRunner.title, 'Blade Runner 2049'); assert.equal(bladeRunner.title, 'Blade Runner 2049');
}); });
it('should translate the tmdb vote_average to rank', () => { it('should translate the tmdb vote_average to rating', () => {
const bladeRunner = convertTmdbToMovie(this.bladeRunnerTmdbMovie); const bladeRunner = Movie.convertFromTmdbResponse(this.bladeRunnerTmdbMovie);
assert.equal(bladeRunner.rank, 7.3); assert.equal(bladeRunner.rating, 7.3);
}); });

View File

@@ -21,7 +21,7 @@ describe('TMDB', function test() {
it('should return the "Blade Runner 2049" year in the collection of popular movies', () => { it('should return the "Blade Runner 2049" year in the collection of popular movies', () => {
this.mockMoviedb.response = popularMovieSuccessResponse; this.mockMoviedb.response = popularMovieSuccessResponse;
const cache = new Cache(this.database); const cache = new Cache(this.database);
const tmdb = new TMDB(cache, 'bogus-api-key', this.mockMoviedb); const tmdb = new TMDB(cache, 'bogus-pi-key', this.mockMoviedb);
return tmdb.popular() return tmdb.popular()
.then(movies => .then(movies =>
assert.equal(movies[0].title, "Blade Runner 2049") assert.equal(movies[0].title, "Blade Runner 2049")

File diff suppressed because it is too large Load Diff

4
yarn.lock Normal file
View File

@@ -0,0 +1,4 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1