diff --git a/api/controllers/BooksController.js b/api/controllers/BooksController.js index e60a688..1c2ed2c 100644 --- a/api/controllers/BooksController.js +++ b/api/controllers/BooksController.js @@ -22,12 +22,15 @@ module.exports = { if (!body.metadata) throw new HttpError(400, 'Missing OPDS metadata') if (!body.metadata['@type'] || body.metadata['@type'] !== 'http://schema.org/Book') throw new HttpError(400, 'Invalid \'@type\': expected \'http://schema.org/Book\'') + let tags = (body.metadata.tags || '').split(/,\s*/) + if (!tags.length && body.metadata.title) tags = body.metadata.title.split(/\s+/).filter(x => x.length < 3) const query = { hostname: host, title: body.metadata.title, author: body.metadata.author, publisher: body.metadata.publisher, identifier: body.metadata.identifier, + tags: JSON.stringify(tags), version: body.metadata.modified.replace(/\D/g, '') } @@ -49,7 +52,10 @@ module.exports = { } sendUpdatesAsync(result) - return res.json(result) + return res.json({ + ...result, + tags: JSON.parse(result.tags || '[]') + }) } catch (e) { if (e instanceof HttpError) return e.send(res) return res.status(500).json({ @@ -67,9 +73,8 @@ async function sendUpdatesAsync (book) { try { const item = targets[i] const user = await User.findOne({ id: item.user }) - const { author: fAuthor, publisher: fPublisher, title: fTitle, identifier: fIsbn, url } = item - const { author: bAuthor, publisher: bPublisher, title: bTitle, identifier: bIsbn, opds } = book - sails.log('sending ' + book.id + ' info to ' + url) + const { author: fAuthor, publisher: fPublisher, title: fTitle, identifier: fIsbn, tags: fTags, url } = item + const { author: bAuthor, publisher: bPublisher, title: bTitle, identifier: bIsbn, tags: bTags, opds } = book if (uriRegex.test(url)) { if (fAuthor && !((bAuthor || '').includes(fAuthor))) continue @@ -77,6 +82,13 @@ async function sendUpdatesAsync (book) { if (fTitle && !((bTitle || '').includes(fTitle))) continue if (fIsbn && !((bIsbn || '').includes(fIsbn))) continue + const filterTags = JSON.parse(fTags || '[]') + if (filterTags.length && filterTags[0].length) { + const otherSet = new Set(filterTags) + if (!([...new Set(JSON.parse(bTags || '[]'))].filter(x => otherSet.has(x)).length)) continue + } + sails.log('sending ' + book.id + ' info to ' + url) + let content = opds const timestamp = Date.now() request.post({ diff --git a/api/controllers/CatalogController.js b/api/controllers/CatalogController.js index bf1a82a..0df32cd 100644 --- a/api/controllers/CatalogController.js +++ b/api/controllers/CatalogController.js @@ -42,7 +42,17 @@ module.exports = { page = Math.abs(+body.page) || 1 delete body.page } - let books = await Book.find(body || {}).skip((page * perPage) - perPage).limit(perPage) + const searchBody = { ...body } + if (searchBody.tags) { + const tags = searchBody.tags.split(/,\s*/) + searchBody.tags = { + or: [ + ...tags.map(tag => ({ contains: tag })), + { in: tags } + ] + } + } + let books = await Book.find(body ? searchBody : {}).skip((page * perPage) - perPage).limit(perPage) if (!books.length) { throw new HttpError(404, 'No books matching those parameters were found.') diff --git a/api/controllers/TargetController.js b/api/controllers/TargetController.js index 6497265..84d4971 100644 --- a/api/controllers/TargetController.js +++ b/api/controllers/TargetController.js @@ -25,13 +25,15 @@ module.exports = { const publisher = req.param('publisher') || '' const title = req.param('title') || '' const isbn = req.param('isbn') || '' + const tags = req.param('tags') || '' if (value.length) { const url = await TargetUrl.update({ id, user: req.user.id }, { url: value, author, publisher, title, - isbn + isbn, + tags: JSON.stringify(tags.split(/,\s*/)) }).fetch() return res.json(url) } else { @@ -52,9 +54,13 @@ module.exports = { }, list: async function (req, res) { try { - const urls = await TargetUrl.find({ + let urls = await TargetUrl.find({ user: req.user.id }) + urls = urls.map(url => ({ + ...url, + tags: JSON.parse(url.tags || '[]') + })) return res.json(urls) } catch (e) { return (new HttpError(500, e.message)).send(res) diff --git a/api/models/Book.js b/api/models/Book.js index 7b360c1..4cf0c29 100644 --- a/api/models/Book.js +++ b/api/models/Book.js @@ -23,7 +23,8 @@ module.exports = { identifier: { type: 'string' }, version: { type: 'string' }, hostname: { type: 'string' }, - opds: { type: 'json' } + opds: { type: 'json' }, + tags: { type: 'string' } // ╔═╗╔╦╗╔╗ ╔═╗╔╦╗╔═╗ // ║╣ ║║║╠╩╗║╣ ║║╚═╗ diff --git a/api/models/TargetUrl.js b/api/models/TargetUrl.js index 413dced..d8a323e 100644 --- a/api/models/TargetUrl.js +++ b/api/models/TargetUrl.js @@ -15,6 +15,7 @@ module.exports = { author: 'string', publisher: 'string', title: 'string', - isbn: 'string' + isbn: 'string', + tags: 'string' } } diff --git a/assets/js/containers/UriListItem.js b/assets/js/containers/UriListItem.js index 961a347..1f95e00 100644 --- a/assets/js/containers/UriListItem.js +++ b/assets/js/containers/UriListItem.js @@ -30,7 +30,7 @@ class UriListItem extends React.Component {
Filters - {['publisher', 'title', 'author', 'isbn'].reduce((a, x) => a + (this.props.item[x] ? 1 : 0), 0) || 'None'} + {['publisher', 'title', 'author', 'isbn', 'tags'].reduce((a, x) => a + (this.props.item[x] ? 1 : 0), 0) || 'None'}
this.props.dispatch(removeUrl(this.props.item.id))} /> @@ -86,6 +86,15 @@ class UriListItem extends React.Component { value={'' + this.props.item.isbn} onChange={(e) => this.props.dispatch(changeUrlField(this.props.item.id, 'isbn', e.target.value))} onBlur={(e) => this.props.dispatch(setUrl(this.props.item))} /> + this.props.dispatch(changeUrlField(this.props.item.id, 'tags', e.target.value.split(/,\s+/)))} + onBlur={(e) => this.props.dispatch(setUrl(this.props.item))} /> ) diff --git a/docs/api.md b/docs/api.md index a8b3938..6b3f65e 100644 --- a/docs/api.md +++ b/docs/api.md @@ -18,6 +18,7 @@ and opds2 publication body with type `application/json`: "title": "Moby-Dick", "author": "Herman Melville", "identifier": "urn:isbn:978031600000X", + "tags": ["story", "classic"], "publisher": "Ebook Publisher.com", "language": "en", "modified": "2015-09-29T17:00:00Z" @@ -46,6 +47,7 @@ The server will respond with either: "id": number, "title": string, "author": string, + "tags": array, "publisher": string, "identifier": string, "version": string, @@ -72,6 +74,7 @@ title: The ebook's title (optional) author: The author (optional) version: A version number (optional) isbn: The ISBN (optional) +tags: Comma-separated search tags (optional) page: The page of results to view (200 results per page) ``` @@ -107,6 +110,7 @@ The server will respond with either: "@type": "http://schema.org/Book", "title": "Moby-Dick", "author": "Herman Melville", + "tags": ["story", "classic"], "publisher": "Ebook Publisher.com", "identifier": "urn:isbn:978031600000X", "language": "en", @@ -165,6 +169,7 @@ HTTP Body: "@type": "http://schema.org/Book", "title": "Moby-Dick", "author": "Herman Melville", + "tags": ["story", "classic"], "publisher": "Ebook Publisher.com", "identifier": "urn:isbn:978031600000X", "language": "en", diff --git a/migrations/20190401161204_add_book_tags.js b/migrations/20190401161204_add_book_tags.js new file mode 100644 index 0000000..eae1e79 --- /dev/null +++ b/migrations/20190401161204_add_book_tags.js @@ -0,0 +1,21 @@ +exports.up = function (knex, Promise) { + return Promise.all([ + knex.schema.table('book', t => { + t.string('tags') + }), + knex.schema.table('targeturl', t => { + t.string('tags') + }) + ]) +} + +exports.down = function (knex, Promise) { + return Promise.all([ + knex.schema.table('book', t => { + t.dropColumns('tags') + }), + knex.schema.table('targeturl', t => { + t.dropColumns('tags') + }) + ]) +}