diff --git a/.gitignore b/.gitignore index 48ac150..da527d3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ auth.json package-lock.json -node_modules/ \ No newline at end of file +node_modules/ +/test.js \ No newline at end of file diff --git a/api/ani/v1/routes/series.js b/api/ani/v1/routes/series.js index 1650cdf..041dc6c 100644 --- a/api/ani/v1/routes/series.js +++ b/api/ani/v1/routes/series.js @@ -3,13 +3,19 @@ const fs = require('fs'); module.exports = (app, parentRouter) => { const router = Router() - .get('/', (req, res) => res.send('/series/:id')); + router.use('/', (req, res, next) => { + req.listData = Object.keys(app.cache.series).map(series => {return { + name: app.cache.series[series].name, + romaji: app.cache.series[series].romaji, + kanji: app.cache.series[series].kanji + }} + ); + return next(); + }, app.util.list(router, '')); //list all anime {id: {name: "", romaji: "", kanji: ""}} router.location = '/series'; fs.readdirSync('./ani/v1/routes/series').filter(file => file.endsWith('.js')) .forEach(route => require(`./series/${route}`)(app, router)); //execute all router functions parentRouter.use('/series', router); - - //TODO get all series }; \ No newline at end of file diff --git a/api/ani/v1/routes/series/add.js b/api/ani/v1/routes/series/add.js new file mode 100644 index 0000000..1935fe5 --- /dev/null +++ b/api/ani/v1/routes/series/add.js @@ -0,0 +1,67 @@ +module.exports = (app, router) => { + const Anime = app.db.models.ani.series; + + router.route('/:id') + .post(app.auth.token, app.auth.perms('series-submit'), app.auth.permsPass('series-approve'), async (req, res) => { + let submitting = req.unauthorized; //if user doesn't have series-approve, they still have perms to submit by this point + + /**REQUIRED ITEMS + * + *!Submissions must include a name and id (romaji included) + * Only altNames, tags, and streaming locations can be omitted + * + * If a request has submissible criteria but not completable + * criteria, it will be treated as a submission even if the user + * is authorized to approve series. + */ + + //TODO submit anyways if incomplete but user has approval permissions + //TODO un-submitted (incomplete) but curated route + + + if (!req.params.id) {return res.status(400).send("You didn't include an anime ID in your request!");} + if (await Anime.findOne({id: req.params.id})) {return res.status(400).send("An anime already exists with that ID!");} + if ( + !req.body //i just ate dinner and i can't even think straight + || !req.body.name || !req.body.romaji || !req.params.id || !req.authenticatedUser || !req.authenticatedUser.id + || !req.body.name.match(/^[\w_\- ]+$/gm) || req.body.name.length > 150 + || req.body.romaji.length > 150 + || !req.params.id.match(/^[\w_\-]+$/gm) || req.params.id.length > 25 + ) {return res.status(400).send("The server cannot accept your request as your body is missing fields or is malformed. Ensure fields aren't too long and that they don't contain illegal characters.");} + + let series = new Anime({ + id: req.params.id, + numericalId: app.cache.seriesCount + 1, + name: req.body.name, + romaji: req.body.romaji, + synopsis: { + by: req.body.synopsis ? req.authenticatedUser.id : null, + synopsis: req.body.synopsis || "A synopsis is not yet available for this series..." + }, + meta: { + submitted: submitting ? req.authenticatedUser.id : false, //TODO make sure to update submitted status + creator: req.authenticatedUser.id, + edits: [{ + user: req.authenticatedUser.id, + action: 'Submitted series', + timestamp: new Date().getTime() + }] + }, + genres: req.body.genres && Array.isArray(req.body.genres) && req.body.genres.length ? req.body.genres : [] + }); + if (!req.body.synopsis || !req.body.genres || !Array.isArray(req.body.genres) || !req.body.genres.length) {series.meta.submitted = req.authenticatedUser.id;} + return series.save().then(async () => { + app.cache.series[series.id] = { + id: series.id, + name: series.name, + romaji: series.romaji, + kanji: series.kanji, + altNames: series.altNames, + genres: series.genres, + tags: series.tags + }; + return res.send(`Your series was successfully ${series.meta.submitted ? 'submitted' : "added"}.`); + }).catch((e) => {console.error(e); res.status(500).send("There was an error trying to process your request. It's likely that our database found something wrong with your body fields, and the server didn't realize. Check your request and try again.");}); + //TODO remove console error + }); +}; \ No newline at end of file diff --git a/api/util/startup/cache.js b/api/util/startup/cache.js index 2cea77e..c96889d 100644 --- a/api/util/startup/cache.js +++ b/api/util/startup/cache.js @@ -13,10 +13,10 @@ module.exports = async app => { const loaders = []; const spin = new spinnies(); - let userCache = spin.add("ar", {text: "Caching Users..."}); + let userCache = spin.add("user", {text: "Caching Users..."}); loaders.push(require('./cache/users')(app, userCache)); - let seriesCache = spin.add("ar", {text: "Caching Series..."}); + let seriesCache = spin.add("series", {text: "Caching Series..."}); loaders.push(require('./cache/series')(app, seriesCache)); await Promise.all(loaders); diff --git a/api/util/startup/cache/series.js b/api/util/startup/cache/series.js index 69e84c8..3e8e002 100644 --- a/api/util/startup/cache/series.js +++ b/api/util/startup/cache/series.js @@ -12,7 +12,6 @@ module.exports = async (app, spinner) => { let {id, name, romaji, kanji, altNames, genres, tags} = series; app.cache.series[series.id] = {id, name, romaji, kanji, altNames, genres, tags}; //keep an in-memory index of series' searchable items app.cache.series[series.id].synopsis = series.synopsis.synopsis; - console.log(app.cache.series[series.id]); spinner.update({text: `${chalk.gray('[PROC]')} >> ${chalk.blueBright(`Cached`)} ${chalk.white(`${amount}`)} ${chalk.blueBright(`ani DB series.`)}`}); app.cache.seriesCount++; amount++; diff --git a/db/ani/series.js b/db/ani/series.js index 258b62c..346aaa7 100644 --- a/db/ani/series.js +++ b/db/ani/series.js @@ -1,33 +1,88 @@ const {Schema} = require("mongoose"); module.exports = (connection) => connection.model('series', new Schema({ - id: {type: String, unique: true}, + id: {type: String, unique: true, required: true, maxLength: 25}, //!REQ + numericalId: {type: Number, unique: true, required: true, min: 1}, //!REQ meta: { locked: {type: Boolean, default: false}, - creator: String, //uid + creator: {type: String, required: true}, //uid //!REQ edits: {type: [{ user: String, //uid timestamp: String, //Date.getTime(), action: String }], default: []}, - completed: {type: Boolean, default: false}, - approved: {type: { - approved: Boolean, - by: String - }, default: false} - }, + completed: {type: Boolean, default: false}, //SUBMISSION completed + approved: {type: Schema.Types.Mixed, default: false}, //boolean or {approved: Boolean, by: } + submitted: Schema.Types.Mixed, //can be false or a string with the ID of the submitter, //!REQ + hidden: {type: Boolean, defualt: false}, + reviewFlags: {type: [{ + by: String, + reason: String, + }], default: []} + }, //!REQ + + name: {type: String, required: true, maxLength: 150}, //!REQ + romaji: {type: String, required: true, maxLength: 150}, //!REQ + kanji: {type: String, maxLength: 150, default: null}, + altNames: {type: [String], default: []}, + + synopsis: {type: { + synopsis: {type: String, required: true, maxLength: 1000}, + by: String //uid + }, required: true}, //if not present, use "Synopsis not available yet" //!REQ + genres: {type: [String], required: true}, //!REQ + //TODOdatabase for genres or cache + tags: {default: [], type: [String]}, + nsfw: {type: Boolean, default: false}, + nsfwReason: {type: String, default: null}, //gore, language, nudity, strong themes - name: String, - romaji: String, - kanji: String, - altNames: [String], - id: Number, + completed: {type: Boolean, default: false}, //SERIES completed + streamAt: {type: [String], default: []}, + publishers: {type: [String], default: []}, + studios: {type: [String], default: []}, + air: { + from: {type: String, default: null}, //absence of start date means anime is confirmed but not released //TODO special handling for unstarted series + to: {type: String, defualt: null} //null indicates still airing; completed: true + non-null "to" value means series is waiting on another season + }, + externalLinks: {type: Object, default: {}}, //streaming services, other databases, etc. //TODO externalLinks + officialSite: {type: String, defualt: null}, + videos: {type: Object, default: {}}, //OPs, EDs, trailers, etc. - synopsis: { - synopsis: String, - by: String + art: { + cover: {type: [String], default: []}, //first item of any list is default item + icon: {type: [String], default: []}, //small 128x128 icon + banner: {type: [String], default: []}, + poster: {type: [String], default: []}, //used for BGs, portrait + widePoster: {type: [String], default: []}, //used for desktop, client should format banners or displays at its discretion if unavailable + display: {type: [String], default: []}, //used for some content BGs, landscape standard desktop res. }, - genres: [String], - tags: [String], + ratings: {type: [{ + user: String, + rating: Number + }], default: []}, //all ratings mapped by user + rating: {type: Number, default: 0}, //automatic collection of user ratings for avg. + watchers: {type: [String], default: []}, //people with this anime on their watching and/or watchlists + likes: {type: Number, default: 0}, //no need to map by user here + reviews: {type: [{ //full review vs rating + user: String, + ratings: { + plot: Number, + characters: Number, + soundtrack: Number, + animation: Number + }, + comments: String + }], default: []}, + + /** + * !API-DEPENDENT FIELDS + * TODO add API-dependent fields for series schema + * + * Stores only IDs and barebones data, requires clients to fetch their contents + */ + + seasons: {type: [String], default: []}, + characters: {type: [String], default: []}, + related: {type: [String], default: []} })); \ No newline at end of file diff --git a/package.json b/package.json index 65c4feb..676fc00 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "discord.js": "^14.7.1", "dreidels": "^0.5.2", "express": "^4.18.2", + "fuse.js": "^6.6.2", "fuzzysort": "^2.0.4", "gradient-string": "^2.0.2", "helmet": "^6.0.1",