Refactor: Python packages should be installed as modules, and not use source files (#149)

* Python packages should be installed as modules, and not use source files

* Use child_process spawn to call command and return data

* Consistent error rejection & resolve around spawn calls.

* Updated README to remove submodule ref & added how to install pip pcks.

* Resovled linting issue

* Removed python-shell package

* Disable stray functionality, unused & has dep to python-shell

* Uncommented/disabled python-shell dependency in strayRepository.
This commit is contained in:
2022-10-02 16:05:29 +02:00
committed by GitHub
parent d6e5bdbd91
commit 3424325be4
9 changed files with 246 additions and 133 deletions

120
README.md
View File

@@ -1,4 +1,3 @@
<h1 align="center"> <h1 align="center">
🌶 seasonedShows 🌶 seasonedShows
</h1> </h1>
@@ -36,63 +35,73 @@
</p> </p>
## <a name="demo-documentation"></a> Demo & Documentation ## <a name="demo-documentation"></a> Demo & Documentation
📺 [DEMO](https://kevinmidboe.com/request)
📺 [DEMO](https://request.movie)
📝 Documentation of the api. 📝 Documentation of the api.
💖 Checkout my [fancy vue.js page](https://github.com/KevinMidboe/seasonedRequest) for interfacing the api. 💖 Checkout my [fancy vue.js page](https://github.com/KevinMidboe/seasoned) for interfacing the api.
## <a name="about"></a> About ## <a name="about"></a> About
This is the backend api for [seasoned request] that allows for uesrs to request movies and shows by fetching movies from themoviedb api and checks them with your plex library to identify if a movie is already present or not. This api allows to search my query, get themoviedb movie lists like popular and now playing, all while checking if the item is already in your plex library. Your friends can create users to see what movies or shows they have requested and searched for.
The api also uses torrent_search to search for matching torrents and returns results from any site or service available from torrent_search. As a admin of the site you can query torrent_search and return a magnet link that can be added to a autoadd folder of your favorite torrent client. This is the backend api for [seasoned request] that allows for uesrs to request movies and shows by fetching movies from themoviedb api and checks them with your plex library to identify if a movie is already present or not. This api allows to search my query, get themoviedb movie lists like popular and now playing, all while checking if the item is already in your plex library. Your friends can create users to see what movies or shows they have requested and searched for.
The api also uses torrent_search to search for matching torrents and returns results from any site or service available from torrent_search. As a admin of the site you can query torrent_search and return a magnet link that can be added to a autoadd folder of your favorite torrent client.
## <a name="key-features"></a> Key features ## <a name="key-features"></a> Key features
### Code ### Code
- Uses [tmdb api](https://www.themoviedb.org/documentation/api) with over 350k movies and 70k tv shows
- Written asynchronously - Uses [tmdb api](https://www.themoviedb.org/documentation/api) with over 350k movies and 70k tv shows
- Uses caching for external requests - Written asynchronously
- Test coverage - Uses caching for external requests
- CI and dependency integrated - Test coverage
- Use either config file or env_variables - CI and dependency integrated
- Use either config file or env_variables
### Functionality ### Functionality
- Queries plex library to check if items exists
- Create admin and normal user accounts - Queries plex library to check if items exists
- [torrent_search](https://github.com/KevinMidboe/torrent_search) to search for torrents - Create admin and normal user accounts
- Fetch curated lists from tmdb - [torrent_search](https://github.com/KevinMidboe/torrent_search) to search for torrents
- Fetch curated lists from tmdb
## <a name="installation"></a> Installation ## <a name="installation"></a> Installation
Before we can use seasonedShows we need to download node and a package manager. For instructions on how to install [yarn](https://yarnpkg.com/en/) or [npm](https://www.npmjs.com) package managers refer to [wiki: install package manager](https://github.com/KevinMidboe/seasonedShows/wiki/Install-package-manager). This api is written with express using node.js as the JavaScript runtime engine. To download node.js head over the the official [node.js download page](https://nodejs.org/en/download/). Before we can use seasonedShows we need to download node and a package manager. For instructions on how to install [yarn](https://yarnpkg.com/en/) or [npm](https://www.npmjs.com) package managers refer to [wiki: install package manager](https://github.com/KevinMidboe/seasonedShows/wiki/Install-package-manager). This api is written with express using node.js as the JavaScript runtime engine. To download node.js head over the the official [node.js download page](https://nodejs.org/en/download/).
### Install seasonedShows ### Install seasonedShows
After you have downloaded a package manager and node.js javascript engine, the following will guide you through how to download, install and run seasonedShows. After you have downloaded a package manager and node.js javascript engine, the following will guide you through how to download, install and run seasonedShows.
### macOS ### macOS
- Open terminal - Open terminal
- Install git. This can be done by running `xcode-select --install` in your favorite terminal. - Install git. This can be done by running `xcode-select --install` in your favorite terminal.
- Install a package manager, refer to this [wiki page] for yarn or [wiki page] for npm - Install a package manager, refer to this [wiki page] for yarn or [wiki page] for npm
- Type: `git clone --recurse-submodules git@github.com:KevinMidboe/seasonedShows.git` - Type: `git clone git@github.com:KevinMidboe/seasonedShows.git`
- Type: `cd seasonedShows/` - Type: `cd seasonedShows/`
- Install required packages - Install required packages
* yarn: `yarn install` - yarn: `yarn install`
* npm: `npm install` - npm: `npm install`
- Start server: - Start server:
* yarn: `yarn start` - yarn: `yarn start`
* npm: `npm run start` - npm: `npm run start`
- seasonedShows will now be running at http://localhost:31459 - seasonedShows will now be running at http://localhost:31459
- To have seasonedShows run headless on startup, check out this wiki page to [install as a daemon]. - To have seasonedShows run headless on startup, check out this wiki page to [install as a daemon].
### Linux ### Linux
- Open terminal - Open terminal
- Install git - Install git
* Ubuntu/Debian: `sudo apt-get install git-core` - Ubuntu/Debian: `sudo apt-get install git-core`
* Fedora: `sudo yum install git` - Fedora: `sudo yum install git`
- Type: `git clone --recurse-submodules git@github.com:KevinMidboe/seasonedShows.git` - Type: `git clone git@github.com:KevinMidboe/seasonedShows.git`
- Type: `cd seasonedShows/` - Type: `cd seasonedShows/`
- Install required packages - Install required packages
* yarn: `yarn install` - yarn: `yarn install`
* npm: `npm install` - npm: `npm install`
- Start server: - Start server:
* yarn: `yarn start` - yarn: `yarn start`
* npm: `npm run start` - npm: `npm run start`
- seasonedShows will now be running at http://localhost:31459 - seasonedShows will now be running at http://localhost:31459
- To have seasonedShows run headless on startup, check out this wiki page to [install as a daemon]. - To have seasonedShows run headless on startup, check out this wiki page to [install as a daemon].
@@ -101,42 +110,73 @@ After you have downloaded a package manager and node.js javascript engine, the f
After you have installed the required packages you will have a node_modules directory with all the packages required in packages.json. After you have installed the required packages you will have a node_modules directory with all the packages required in packages.json.
### Requirements ### Requirements
- Node 7.6 < [wiki page]
- Plex library
## <a name="setup"></a> Setup and/ configuration - Node 18 < [wiki page]
There is a config file template, what the values mean and how to change them. - Plex library
Also show how to hide file from git if not want to show up as uncommitted file. - Optional:
Also set variables in environment. - redis
- deluge
- jackett
## <a name="setup"></a> Setup and configuration
Make a copy of configuration file in `configurations/` folder.
For use during development and with `yarn dev` command:
```bash
cp configurations/development.json.example configurations/development.json
```
For use during production and with `yarn start` command:
```bash
cp configurations/development.json.example configurations/production.json
```
Most important values to change here is adding [TMDB api key](https://developers.themoviedb.org/3/getting-started/introduction) and plex server IP.
### Optional setup
To allow authenticated or admin users add items [delugeClient](https://github.com/kevinmidboe/delugeClient) & [torrentSearch](https://github.com/KevinMidboe/torrent_search) can be setup to fetch and add magnet files. Both require python version >= 3.8 and can be downloaded using following pip command:
```bash
pip3 install delugeClient_kevin torrent_search
```
Both of these need to be configured, view their separate README's or find configuration files under `$HOME/.config/`.
## <a name="running"></a> Running/using ## <a name="running"></a> Running/using
yarn/npm start. (can also say this above) yarn/npm start. (can also say this above)
How to create service on linux. This means that How to create service on linux. This means that
## <a name="daemon"></a> Setup a daemon ## <a name="daemon"></a> Setup a daemon
The next step is to setup seasonedShows api to run in the background as a daemon. I have written a [wiki page](https://github.com/KevinMidboe/seasonedShows/wiki/Install-as-a-daemon) on how to create a daemon on several unix distors and macOS. The next step is to setup seasonedShows api to run in the background as a daemon. I have written a [wiki page](https://github.com/KevinMidboe/seasonedShows/wiki/Install-as-a-daemon) on how to create a daemon on several unix distors and macOS.
*Please don't hesitate to add your own system if you get it running on something that is not yet lists on the formentioned wiki page.* _Please don't hesitate to add your own system if you get it running on something that is not yet lists on the formentioned wiki page._
## <a name="contributing"></a> Contributing ## <a name="contributing"></a> Contributing
- Fork it! - Fork it!
- Create your feature branch: git checkout -b my-new-feature - Create your feature branch: git checkout -b my-new-feature
- Commit your changes: git commit -am 'Add some feature' - Commit your changes: git commit -am 'Add some feature'
- Push to the branch: git push origin my-new-feature - Push to the branch: git push origin my-new-feature
- Submit a pull request - Submit a pull request
## Api documentation ## Api documentation
The goal of this project is to create a full custom stack that can to everything surround downloading, organizing and notifiyng of new media. From the top down we have a website using [tmdb](https://www.themoviedb.com) api to search for from over 350k movies and 70k tv shows. Using [hjone72](https://github.com/hjone72/PlexAuth) great PHP reverse proxy we can have a secure way of allowing users to login with their plex credentials which limits request capabilites to only users that are authenticated to use your plex library.
The goal of this project is to create a full custom stack that can to everything surround downloading, organizing and notifiyng of new media. From the top down we have a website using [tmdb](https://www.themoviedb.com) api to search for from over 350k movies and 70k tv shows. Using [hjone72](https://github.com/hjone72/PlexAuth) great PHP reverse proxy we can have a secure way of allowing users to login with their plex credentials which limits request capabilites to only users that are authenticated to use your plex library. seasonedShows is a intelligent organizer for your tv show episodes. It is made to automate and simplify to process of renaming and moving newly downloaded tv show episodes following Plex file naming and placement.
seasonedShows is a intelligent organizer for your tv show episodes. It is made to automate and simplify to process of renaming and moving newly downloaded tv show episodes following Plex file naming and placement.
So this is a multipart system that lets your plex users request movies, and then from the admin page the owner can. So this is a multipart system that lets your plex users request movies, and then from the admin page the owner can.
## Installation ## Installation
There are two main ways of
There are two main ways of
## Architecture ## Architecture
The flow of the system will first check for new folders in your tv shows directory, if a new file is found it's contents are analyzed, stored and tweets suggested changes to it's contents to use_admin. The flow of the system will first check for new folders in your tv shows directory, if a new file is found it's contents are analyzed, stored and tweets suggested changes to it's contents to use_admin.
Then there is a script for looking for replies on twitter by user_admin, if caanges are needed, it handles the changes specified and updates dtabbase. Then there is a script for looking for replies on twitter by user_admin, if caanges are needed, it handles the changes specified and updates dtabbase.

View File

@@ -1,6 +1,6 @@
{ {
"database": { "database": {
"host": "../shows.db" "host": "./shows.db"
}, },
"redis": { "redis": {
"host": "localhost", "host": "localhost",

View File

@@ -27,7 +27,6 @@
"form-data": "^2.5.1", "form-data": "^2.5.1",
"jsonwebtoken": "^8.5.1", "jsonwebtoken": "^8.5.1",
"km-moviedb": "^0.2.12", "km-moviedb": "^0.2.12",
"python-shell": "^0.5.0",
"raven": "^2.4.2", "raven": "^2.4.2",
"redis": "^3.0.2", "redis": "^3.0.2",
"sqlite3": "^5.0.1" "sqlite3": "^5.0.1"

View File

@@ -1,48 +1,121 @@
import http from "http"; import http from "http";
import { URL } from "url"; import { URL } from "url";
import PythonShell from "python-shell"; import { spawn } from "child_process";
import establishedDatabase from "../database/database.js"; import establishedDatabase from "../database/database.js";
import cache from "../cache/redis.js"; import cache from "../cache/redis.js";
function getMagnetFromURL(url) { class SearchPackageNotFoundError extends Error {
return new Promise(resolve => { constructor() {
const options = new URL(url); const message = "Search is not setup, view logs.";
if (options.protocol.includes("magnet")) resolve(url); super(message);
const warningMessage = `Warning! Package 'torrentSearch' not setup! View project README.`;
console.log(warningMessage); /* eslint-disable-line no-console */
}
}
class AddMagnetPackageNotFoundError extends Error {
constructor() {
const message = "Adding magnet is not setup, view logs.";
super(message);
const warningMessage = `Warning! Package 'delugeClient' not setup! View project README.`;
console.log(warningMessage); /* eslint-disable-line no-console */
}
}
class InvalidMagnetUrlError extends Error {
constructor() {
const message = "Invalid magnet url.";
super(message);
}
}
class UnexpectedScriptError extends Error {
constructor(_package, error = null) {
const message = `There was an unexpected error while running package: ${_package}`;
super(message);
this.error = error;
// console.log("Unexpected script error:", error);
}
}
function getMagnetFromURL(url) {
const options = new URL(url);
if (options?.protocol?.includes("magnet")) return Promise.resolve(url);
return new Promise((resolve, reject) => {
http.get(options, res => { http.get(options, res => {
if (res.statusCode === 301 || res.statusCode === 302) { if (res.statusCode !== 301 && res.statusCode !== 302)
resolve(res.headers.location); reject(new InvalidMagnetUrlError());
} if (!res?.headers?.location?.includes("magnet"))
reject(new InvalidMagnetUrlError());
return resolve(res.headers.location);
}); });
}); });
} }
async function find(searchterm, callback) { function removeNewLineListItem(list) {
const options = { return list.map(el => el.replace("\n", "")).filter(el => el.length !== 0);
pythonPath: "../torrent_search/env/bin/python3",
scriptPath: "../torrent_search",
args: [searchterm, "-s", "jackett", "--print"]
};
PythonShell.run("torrentSearch/search.py", options, callback);
// PythonShell does not support return
} }
async function callPythonAddMagnet(url, callback) { function decodeBufferListToString(bufferList) {
getMagnetFromURL(url) let data = bufferList.map(bufferElement => bufferElement.toString());
.then(magnet => { if (data.length === 0) return null;
const options = {
pythonPath: "../delugeClient/env/bin/python3",
scriptPath: "../delugeClient",
args: ["add", magnet]
};
PythonShell.run("deluge_cli.py", options, callback); data = removeNewLineListItem(data);
}) return data.join("");
.catch(err => { }
throw new Error(err);
}); function addMagnetScript(magnet, callback) {
const data = [];
let error = null;
const args = ["add", magnet];
const addMagnet = spawn("delugeclient", args);
addMagnet.stdout.on("data", bufferedData => data.push(bufferedData));
addMagnet.stderr.on("data", bufferedError => {
error = bufferedError.toString();
});
addMagnet.on("exit", () => callback(error, decodeBufferListToString(data)));
addMagnet.on("error", error => {
callback(error);
});
}
function handleAddMagnetScriptError(error) {
if (error?.code === "ENOENT") return new AddMagnetPackageNotFoundError();
return new UnexpectedScriptError("delugeClient", error);
}
function searchScript(searchterm, callback) {
const data = [];
let error = null;
const args = [searchterm, "-s", "jackett", "--print"];
const torrentSearch = spawn("torrentsearch", args);
torrentSearch.stdout.on("data", bufferedData => data.push(bufferedData));
torrentSearch.stderr.on("data", bufferedError => {
error = bufferedError.toString();
});
torrentSearch.on("exit", () =>
callback(error, decodeBufferListToString(data))
);
torrentSearch.on("error", error => callback(error));
}
function handleSearchScriptError(error) {
if (error?.code === "ENOENT") return new SearchPackageNotFoundError();
return new UnexpectedScriptError("torrentSearch", error);
} }
export async function SearchPiratebay(_query) { export async function SearchPiratebay(_query) {
@@ -53,47 +126,46 @@ export async function SearchPiratebay(_query) {
} }
const cacheKey = `pirate/${query}`; const cacheKey = `pirate/${query}`;
try {
const hit = await cache.get(cacheKey);
return new Promise((resolve, reject) => if (hit) {
cache return Promise.resolve(hit);
.get(cacheKey) }
.then(resolve) } catch (_) {}
.catch(() =>
find(query, (err, results) => {
if (err) {
console.log("THERE WAS A FUCKING ERROR!\n", err); // eslint-disable-line no-console
reject(Error("There was a error when searching for torrents"));
}
if (results) { return new Promise((resolve, reject) => {
const jsonData = JSON.parse(results[1], null, "\t"); searchScript(query, (error, results) => {
cache.set(cacheKey, jsonData); if (error || !results) return reject(handleSearchScriptError(error));
resolve(jsonData);
} const jsonData = JSON.parse(results, null, "\t");
}) cache.set(cacheKey, jsonData);
) return resolve(jsonData);
); });
});
} }
export function AddMagnet(magnet, name, tmdbId) { export async function AddMagnet(magnetUrl, name, tmdbId) {
return new Promise((resolve, reject) => const magnet = await getMagnetFromURL(magnetUrl);
callPythonAddMagnet(magnet, (err, results) => { const insertRequestedMagnetQuery =
if (err) { "INSERT INTO requested_torrent(magnet, torrent_name, tmdb_id) VALUES (?,?,?)";
/* eslint-disable no-console */
console.log(err);
reject(Error("Enable to add torrent", err));
}
/* eslint-disable no-console */
console.log("result/error:", err, results);
return new Promise((resolve, reject) => {
addMagnetScript(magnet, (error, result) => {
if (error || !result) return reject(handleAddMagnetScriptError(error));
const magnetHash = result; // TODO save to database
const database = establishedDatabase; const database = establishedDatabase;
const insertQuery = return database
"INSERT INTO requested_torrent(magnet,torrent_name,tmdb_id) VALUES (?,?,?)"; .run(insertRequestedMagnetQuery, [magnet, name, tmdbId])
.catch(error => reject(error))
const response = database.run(insertQuery, [magnet, name, tmdbId]); .then(() =>
console.log(`Response from requsted_torrent insert: ${response}`); resolve({
success: true,
resolve({ success: true }); message: "Successfully added magnet",
}) hash: magnetHash
); })
);
});
});
} }

View File

@@ -1,5 +1,5 @@
import assert from "assert"; import assert from "assert";
import pythonShell from "python-shell"; // import pythonShell from "python-shell";
import Stray from "./stray.js"; import Stray from "./stray.js";
import establishedDatabase from "../database/database.js"; import establishedDatabase from "../database/database.js";
@@ -48,17 +48,17 @@ class StrayRepository {
return this.database.get(this.queries.checkVerified, strayId).then(row => { return this.database.get(this.queries.checkVerified, strayId).then(row => {
assert.notEqual(row, undefined, `Stray '${strayId}' already verified.`); assert.notEqual(row, undefined, `Stray '${strayId}' already verified.`);
const options = { // const options = {
pythonPath: "../app/env/bin/python3", // pythonPath: "../app/env/bin/python3",
args: [strayId] // args: [strayId]
}; // };
pythonShell.run("../app/moveSeasoned.py", options, (err, results) => { // pythonShell.run("../app/moveSeasoned.py", options, (err, results) => {
if (err) throw err; // if (err) throw err;
// TODO Add error handling!! StrayRepository.ERROR // // TODO Add error handling!! StrayRepository.ERROR
// results is an array consisting of messages collected during execution // // results is an array consisting of messages collected during execution
console.log("results: %j", results); // console.log("results: %j", results);
}); // });
return this.database.run(this.queries.verify, strayId); return this.database.run(this.queries.verify, strayId);
}); });

View File

@@ -40,9 +40,10 @@ import ShowInfoController from "./controllers/show/info.js";
import PersonCreditsController from "./controllers/person/credits.js"; import PersonCreditsController from "./controllers/person/credits.js";
import PersonInfoController from "./controllers/person/info.js"; import PersonInfoController from "./controllers/person/info.js";
import SeasonedAllController from "./controllers/seasoned/readStrays.js"; // TODO refactor python-shell dependency or remove stray admin functions
import SeasonedInfoController from "./controllers/seasoned/strayById.js"; // import SeasonedAllController from "./controllers/seasoned/readStrays.js";
import SeasonedVerifyController from "./controllers/seasoned/verifyStray.js"; // import SeasonedInfoController from "./controllers/seasoned/strayById.js";
// import SeasonedVerifyController from "./controllers/seasoned/verifyStray.js";
import PlexSearchController from "./controllers/plex/search.js"; import PlexSearchController from "./controllers/plex/search.js";
import PlexFetchRequestedController from "./controllers/plex/fetchRequested.js"; import PlexFetchRequestedController from "./controllers/plex/fetchRequested.js";
@@ -165,9 +166,9 @@ router.get(
/** /**
* Seasoned * Seasoned
*/ */
router.get("/v1/seasoned/all", SeasonedAllController); // router.get("/v1/seasoned/all", SeasonedAllController);
router.get("/v1/seasoned/:strayId", SeasonedInfoController); // router.get("/v1/seasoned/:strayId", SeasonedInfoController);
router.post("/v1/seasoned/verify/:strayId", SeasonedVerifyController); // router.post("/v1/seasoned/verify/:strayId", SeasonedVerifyController);
router.get("/v2/search/", SearchMultiController); router.get("/v2/search/", SearchMultiController);
router.get("/v2/search/movie", SearchMovieController); router.get("/v2/search/movie", SearchMovieController);

View File

@@ -14,7 +14,10 @@ function addMagnet(req, res) {
AddMagnet(magnet, name, tmdbId) AddMagnet(magnet, name, tmdbId)
.then(result => res.send(result)) .then(result => res.send(result))
.catch(error => { .catch(error => {
res.status(500).send({ success: false, message: error.message }); res.status(error?.statusCode || 500).send({
success: false,
message: error?.message || "Unexpected error while adding magnet."
});
}); });
} }

View File

@@ -22,7 +22,10 @@ function updateRequested(req, res) {
res.send({ success: true, results: result }); res.send({ success: true, results: result });
}) })
.catch(error => { .catch(error => {
res.status(401).send({ success: false, message: error.message }); res.status(error?.statusCode || 500).send({
success: false,
message: error?.message || "Unexpected error while searching."
});
}); });
} }

View File

@@ -5742,11 +5742,6 @@ punycode@^2.1.0, punycode@^2.1.1:
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
python-shell@^0.5.0:
version "0.5.0"
resolved "https://registry.yarnpkg.com/python-shell/-/python-shell-0.5.0.tgz#461983bafd092010bc2760c365b13e7d50aab231"
integrity sha512-+jgmFZvwk1yMBBDisDlkXXMYv1eEJKbGCtwHLppGIyEV83cKeX9hjOjfR2yONWK3yQFhum0M2r7UE0U//hiK9w==
qs@6.5.2: qs@6.5.2:
version "6.5.2" version "6.5.2"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"