diff --git a/.eslintrc b/.eslintrc index 65f664232..8714ff58e 100644 --- a/.eslintrc +++ b/.eslintrc @@ -15,6 +15,8 @@ "ignoreUrls": true }], "vue/html-indent": ["error", 4], + "vue/multiline-html-element-content-newline": 0, + "vue/singleline-html-element-content-newline": 0, "no-param-reassign": ["error", { "props": true, "ignorePropertyModificationsFor": ["state", "acc"] diff --git a/assets/components/header/header.vue b/assets/components/header/header.vue index 465491601..70281e32c 100644 --- a/assets/components/header/header.vue +++ b/assets/components/header/header.vue @@ -1,6 +1,11 @@ @@ -13,7 +18,13 @@ padding: 1rem; } +.logo-link { + color: inherit; + text-decoration: none; +} + .logo { + display: inline-block; margin: 0; } diff --git a/assets/components/home/home.vue b/assets/components/home/home.vue index 88cbe3d26..897a2cabf 100644 --- a/assets/components/home/home.vue +++ b/assets/components/home/home.vue @@ -33,10 +33,17 @@ class="scene-link" > + +
No thumbnail available
@@ -112,7 +119,7 @@ export default { .scenes { display: grid; - grid-template-columns: repeat(auto-fit, minmax(18rem, 1fr)); + grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr)); grid-gap: 1rem; } @@ -134,10 +141,15 @@ export default { .scene-thumbnail { width: 100%; height: 200px; - display: block; + display: flex; + justify-content: center; + align-items: center; + object-fit: cover; background-position: center; background-size: cover; - object-fit: cover; + background-color: $shadow-hint; + color: $shadow; + text-shadow: 1px 1px 0 $highlight; } .scene-row { @@ -168,6 +180,7 @@ export default { .scene-site { border-radius: 0 0 .25rem 0; + font-weight: bold; } .scene-date { @@ -219,7 +232,7 @@ export default { } .scene-actor:not(:last-of-type)::after, -.scene-tag:not(:last-of-type):after { +.scene-tag:not(:last-child):after { content: ","; } diff --git a/assets/components/release/release.vue b/assets/components/release/release.vue index 8699f2ece..d2ba23c15 100644 --- a/assets/components/release/release.vue +++ b/assets/components/release/release.vue @@ -1,5 +1,147 @@ + + + + diff --git a/assets/css/_theme.scss b/assets/css/_theme.scss index b9689a33b..f15832478 100644 --- a/assets/css/_theme.scss +++ b/assets/css/_theme.scss @@ -2,3 +2,9 @@ $primary: #ff886c; $text: #222; $text-contrast: #fff; + +$shadow: rgba(0, 0, 0, .5); +$shadow-weak: rgba(0, 0, 0, .2); +$shadow-hint: rgba(0, 0, 0, .1); + +$highlight: rgba(255, 255, 255, .5); diff --git a/assets/css/style.scss b/assets/css/style.scss index 69f3e2ff7..4626b95b5 100644 --- a/assets/css/style.scss +++ b/assets/css/style.scss @@ -6,7 +6,9 @@ body { } body { + color: $text; margin: 0; + font-family: Verdana, sans-serif; } .nolist { diff --git a/assets/js/main.js b/assets/js/main.js index e91668308..7d23d2ff0 100644 --- a/assets/js/main.js +++ b/assets/js/main.js @@ -12,6 +12,16 @@ function init() { const store = initStore(router); Vue.mixin({ + watch: { + pageTitle(title) { + if (title) { + document.title = `Porn Radar - ${title}`; + return; + } + + document.title = 'Porn Radar'; + }, + }, methods: { formatDate: (date, format) => dayjs(date).format(format), }, diff --git a/assets/js/router.js b/assets/js/router.js index 839e40670..1fa4b8941 100644 --- a/assets/js/router.js +++ b/assets/js/router.js @@ -12,18 +12,22 @@ const routes = [ { path: '/', component: Home, + name: 'home', }, { path: '/scene/:releaseId', component: Release, + name: 'release', }, { path: '/movie/:releaseId', component: Release, + name: 'release', }, { path: '/actor/:actorSlug', component: Actor, + name: 'actor', }, { path: '*', diff --git a/config/default.js b/config/default.js index 8bedd3051..4645f2500 100644 --- a/config/default.js +++ b/config/default.js @@ -81,7 +81,7 @@ module.exports = { width: 30, }, ], - thumbnailPath: '/home/niels/Pictures/traxxx', + photoPath: '/mnt/stor/Pictures/traxxx', filename: { dateFormat: 'DD-MM-YYYY', actorsJoin: ', ', diff --git a/migrations/20190325001339_releases.js b/migrations/20190325001339_releases.js index 3368bc2ae..3deb23331 100644 --- a/migrations/20190325001339_releases.js +++ b/migrations/20190325001339_releases.js @@ -12,6 +12,9 @@ exports.up = knex => Promise.resolve() table.integer('alias_for', 8) .references('id') .inTable('actors'); + + table.integer('photos', 3) + .defaultTo(0); })) .then(() => knex.schema.createTable('directors', (table) => { table.increments('id', 8); @@ -20,6 +23,9 @@ exports.up = knex => Promise.resolve() table.integer('alias_for', 8) .references('id') .inTable('directors'); + + table.integer('photos', 3) + .defaultTo(0); })) .then(() => knex.schema.createTable('tags_groups', (table) => { table.string('group', 20) @@ -89,6 +95,9 @@ exports.up = knex => Promise.resolve() table.integer('duration') .unsigned(); + table.integer('photos', 3) + .defaultTo(0); + table.integer('likes') .unsigned(); @@ -114,6 +123,19 @@ exports.up = knex => Promise.resolve() .references('id') .inTable('actors'); })) + .then(() => knex.schema.createTable('directors_associated', (table) => { + table.increments('id', 16); + + table.integer('release_id', 12) + .notNullable() + .references('id') + .inTable('releases'); + + table.integer('director_id', 8) + .notNullable() + .references('id') + .inTable('directors'); + })) .then(() => knex.schema.createTable('tags_associated', (table) => { table.string('tag_id', 20) .notNullable() @@ -131,6 +153,7 @@ exports.up = knex => Promise.resolve() exports.down = knex => Promise.resolve() .then(() => knex.schema.dropTable('tags_associated')) + .then(() => knex.schema.dropTable('directors_associated')) .then(() => knex.schema.dropTable('actors_associated')) .then(() => knex.schema.dropTable('tags')) .then(() => knex.schema.dropTable('tags_groups')) diff --git a/public/css/style.css b/public/css/style.css index f0d03264d..f084b18fa 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -1,6 +1,6 @@ .scenes[data-v-5533e378] { display: grid; - grid-template-columns: repeat(auto-fit, minmax(18rem, 1fr)); + grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr)); grid-gap: 1rem; } .scene[data-v-5533e378] { @@ -19,11 +19,16 @@ .scene-thumbnail[data-v-5533e378] { width: 100%; height: 200px; - display: block; - background-position: center; - background-size: cover; + display: flex; + justify-content: center; + align-items: center; -o-object-fit: cover; object-fit: cover; + background-position: center; + background-size: cover; + background-color: rgba(0, 0, 0, 0.1); + color: rgba(0, 0, 0, 0.5); + text-shadow: 1px 1px 0 rgba(255, 255, 255, 0.5); } .scene-row[data-v-5533e378] { display: flex; @@ -50,6 +55,7 @@ } .scene-site[data-v-5533e378] { border-radius: 0 0 .25rem 0; + font-weight: bold; } .scene-date[data-v-5533e378] { border-radius: 0 0 0 .25rem; @@ -91,7 +97,7 @@ font-size: .75rem; } .scene-actor[data-v-5533e378]:not(:last-of-type)::after, -.scene-tag[data-v-5533e378]:not(:last-of-type):after { +.scene-tag[data-v-5533e378]:not(:last-child):after { content: ","; } .actor-link[data-v-5533e378], @@ -102,12 +108,49 @@ width: 300px; } +.banner[data-v-2bc41e74] { + white-space: nowrap; + overflow-x: auto; + margin: 0 0 1rem 0; + scrollbar-width: none; + box-shadow: 0 0 3px rgba(0, 0, 0, 0.5); +} +.banner[data-v-2bc41e74]::-webkit-scrollbar { + display: none; +} +.banner-item[data-v-2bc41e74] { + height: 20rem; + vertical-align: middle; +} +.row[data-v-2bc41e74] { + display: block; + padding: 0 1rem; + margin: 0 0 .5rem 0; +} +.actors[data-v-2bc41e74], +.tags[data-v-2bc41e74] { + list-style: none; +} +.actor[data-v-2bc41e74], +.tag[data-v-2bc41e74] { + display: inline-block; + text-transform: capitalize; +} +.actor[data-v-2bc41e74]:not(:last-child)::after, + .tag[data-v-2bc41e74]:not(:last-child)::after { + content: ','; + display: inline-block; + width: .6rem; +} + html, body { height: 100%; } body { - margin: 0; } + color: #222; + margin: 0; + font-family: Verdana, sans-serif; } .nolist { list-style: none; @@ -125,7 +168,12 @@ body { background: #ff886c; padding: 1rem; } +.logo-link[data-v-10b7ec04] { + color: inherit; + text-decoration: none; +} .logo[data-v-10b7ec04] { + display: inline-block; margin: 0; } diff --git a/src/fetch-releases.js b/src/fetch-releases.js index 53ce21409..60783a7ee 100644 --- a/src/fetch-releases.js +++ b/src/fetch-releases.js @@ -87,13 +87,16 @@ async function storeReleases(releases = []) { title: release.title, date: release.date, description: release.description, - director: release.director, + // director: release.director, duration: release.duration, + photos: release.photos ? release.photos.length : 0, likes: release.rating && release.rating.likes, dislikes: release.rating && release.rating.dislikes, rating: release.rating && release.rating.stars, }; + console.log(`Storing (${release.site.name}, ${release.id}) "${release.title}"`); + const releaseQuery = `${knex('releases').insert(curatedRelease).toString()} ON CONFLICT DO NOTHING RETURNING *`; const releaseEntry = await knex.raw(releaseQuery); @@ -117,15 +120,18 @@ async function storeReleases(releases = []) { }))); } - if (release.thumbnails && release.thumbnails.length > 0) { - const thumbnailPath = path.join(config.thumbnailPath, release.site.id, releaseEntry.rows[0].id.toString()); + if (release.photos && release.photos.length > 0) { + const photoPath = path.join(config.photoPath, release.site.id, releaseEntry.rows[0].id.toString()); - await fs.mkdir(thumbnailPath, { recursive: true }); + await fs.mkdir(photoPath, { recursive: true }); - await Promise.map(release.thumbnails, async (thumbnailUrl, index) => { - const res = await bhttp.get(thumbnailUrl); + console.log(`Storing photos for (${release.site.name}, ${release.id}) "${release.title}"`); - await fs.writeFile(path.join(thumbnailPath, `${index}.jpg`), res.body); + await Promise.map(release.photos, async (photoUrl, index) => { + const res = await bhttp.get(photoUrl); + await fs.writeFile(path.join(photoPath, `${index + 1}.jpg`), res.body); + + return photoUrl; }, { concurrency: 2, }); diff --git a/src/fetch-scene.js b/src/fetch-scene.js index 185f9e64f..463b5cef5 100644 --- a/src/fetch-scene.js +++ b/src/fetch-scene.js @@ -82,14 +82,16 @@ async function storeRelease(release) { title: release.title, date: release.date, description: release.description, - director: release.director, + // director: release.director, duration: release.duration, + photos: release.photos ? release.photos.length : 0, likes: release.rating && release.rating.likes, dislikes: release.rating && release.rating.dislikes, rating: release.rating && release.rating.stars, }; - console.log('Saving releases to database'); + console.log('Saving release to database'); + await knex.raw(`${knex('releases').insert(curatedRelease).toString()} ON CONFLICT (site_id, shoot_id) DO UPDATE SET description = EXCLUDED.description, likes = EXCLUDED.likes, diff --git a/src/releases.js b/src/releases.js index a7ea0c1d4..76386486f 100644 --- a/src/releases.js +++ b/src/releases.js @@ -24,6 +24,7 @@ async function curateRelease(release) { actors, director: release.director, tags, + photos: release.photos, rating: { likes: release.likes, dislikes: release.dislikes, diff --git a/src/scrapers/julesjordan.js b/src/scrapers/julesjordan.js index 5f4844913..2bc7afbc1 100644 --- a/src/scrapers/julesjordan.js +++ b/src/scrapers/julesjordan.js @@ -11,9 +11,9 @@ function scrapeLatest(html, site) { const scenesElements = $('.update_details').toArray(); return scenesElements.map((element) => { - const thumbnailElement = $(element).find('a img.thumbs'); - const thumbnailCount = Number(thumbnailElement.attr('cnt')); - const thumbnails = Array.from({ length: thumbnailCount }, (value, index) => thumbnailElement.attr(`src${index}_1x`)).filter(thumbnailUrl => thumbnailUrl !== undefined); + const photoElement = $(element).find('a img.thumbs'); + const photoCount = Number(photoElement.attr('cnt')); + const photos = Array.from({ length: photoCount }, (value, index) => photoElement.attr(`src${index}_1x`)).filter(photoUrl => photoUrl !== undefined); const sceneLinkElement = $(element).children('a').eq(1); const url = sceneLinkElement.attr('href'); @@ -36,7 +36,7 @@ function scrapeLatest(html, site) { actors, date, site, - thumbnails, + photos, }; }); } @@ -46,9 +46,9 @@ function scrapeUpcoming(html, site) { const scenesElements = $('#coming_soon_carousel').find('.table').toArray(); return scenesElements.map((element) => { - const thumbnailElement = $(element).find('a img.thumbs'); - const thumbnailCount = Number(thumbnailElement.attr('cnt')); - const thumbnails = Array.from({ length: thumbnailCount }, (value, index) => thumbnailElement.attr(`src${index}_1x`)).filter(thumbnailUrl => thumbnailUrl !== undefined); + const photoElement = $(element).find('a img.thumbs'); + const photoCount = Number(photoElement.attr('cnt')); + const photos = Array.from({ length: photoCount }, (value, index) => photoElement.attr(`src${index}_1x`)).filter(photoUrl => photoUrl !== undefined); const shootId = $(element).find('.upcoming_updates_thumb').attr('id').match(/\d+/)[0]; @@ -77,7 +77,7 @@ function scrapeUpcoming(html, site) { title, date, actors, - thumbnails, + photos, rating: null, site, }; diff --git a/src/scrapers/kink.js b/src/scrapers/kink.js index 24fbed0a8..da3606278 100644 --- a/src/scrapers/kink.js +++ b/src/scrapers/kink.js @@ -18,7 +18,7 @@ function scrapeLatest(html, site) { const shootId = href.split('/')[2]; const title = sceneLinkElement.text().trim(); - const thumbnails = $(element).find('.rollover .roll-image').map((thumbnailIndex, thumbnailElement) => $(thumbnailElement).attr('data-imagesrc')).toArray(); + const photos = $(element).find('.rollover .roll-image').map((photoIndex, photoElement) => $(photoElement).attr('data-imagesrc')).toArray(); const date = moment.utc($(element).find('.date').text(), 'MMM DD, YYYY').toDate(); const actors = $(element).find('.shoot-thumb-models a').map((actorIndex, actorElement) => $(actorElement).text()).toArray(); @@ -35,7 +35,7 @@ function scrapeLatest(html, site) { title, actors, date, - thumbnails, + photos, rating: { stars, }, @@ -52,7 +52,7 @@ async function scrapeScene(html, url, shootId, ratingRes, site) { const title = $('h1.shoot-title span.favorite-button').attr('data-title'); const actorsRaw = $('.shoot-info p.starring'); - const thumbnails = $('.gallery .thumb img').map((thumbnailIndex, thumbnailElement) => `https://cdnp.kink.com${$(thumbnailElement).attr('data-image-file')}`).toArray(); + const photos = $('.gallery .thumb img').map((photoIndex, photoElement) => `https://cdnp.kink.com${$(photoElement).attr('data-image-file')}`).toArray(); const trailerVideo = $('.player span[data-type="trailer-src"]').attr('data-url'); const trailerPoster = $('.player video#kink-player').attr('poster'); @@ -85,7 +85,7 @@ async function scrapeScene(html, url, shootId, ratingRes, site) { date, actors, description, - thumbnails, + photos, trailer: { video: { default: trailerVideo, diff --git a/src/web/server.js b/src/web/server.js index 7b7a01220..b76da2c30 100644 --- a/src/web/server.js +++ b/src/web/server.js @@ -12,7 +12,7 @@ function initServer() { const app = express(); const router = Router(); - router.use(express.static(config.thumbnailPath)); + router.use(express.static(config.photoPath)); router.use(express.static('public')); router.use(bodyParser.json({ strict: false }));