Compare commits
2 Commits
2ac879d276
...
f684923a8a
Author | SHA1 | Date |
---|---|---|
|
f684923a8a | |
|
a223f933ce |
|
@ -136,7 +136,7 @@ export default {
|
||||||
|
|
||||||
.tiles {
|
.tiles {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, 10rem);
|
grid-template-columns: repeat(auto-fill, minmax(10rem, 1fr));
|
||||||
grid-gap: 0 .5rem;
|
grid-gap: 0 .5rem;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
@ -209,7 +209,7 @@ export default {
|
||||||
|
|
||||||
@media(max-width: $breakpoint) {
|
@media(max-width: $breakpoint) {
|
||||||
.tiles {
|
.tiles {
|
||||||
grid-template-columns: repeat(auto-fit, minmax(8rem, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(8rem, 1fr));
|
||||||
}
|
}
|
||||||
|
|
||||||
.genders {
|
.genders {
|
||||||
|
|
|
@ -400,13 +400,14 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
.actors {
|
.actors {
|
||||||
display: flex;
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(10rem, 1fr));
|
||||||
|
grid-gap: 1rem;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.actor {
|
.actor {
|
||||||
width: 10rem;
|
|
||||||
margin: 0 1rem .5rem 0;
|
margin: 0 1rem .5rem 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -477,5 +478,9 @@ export default {
|
||||||
width: 15rem;
|
width: 15rem;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.actors {
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(8rem, 1fr));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -22,8 +22,10 @@
|
||||||
<span
|
<span
|
||||||
v-else
|
v-else
|
||||||
v-tooltip.top="actor.name"
|
v-tooltip.top="actor.name"
|
||||||
class="handle name"
|
class="handle"
|
||||||
>{{ actor.name }}</span>
|
>
|
||||||
|
<span class="name">{{ actor.name }}</span>
|
||||||
|
</span>
|
||||||
|
|
||||||
<div class="avatar-container">
|
<div class="avatar-container">
|
||||||
<img
|
<img
|
||||||
|
@ -103,12 +105,28 @@ export default {
|
||||||
.actor {
|
.actor {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
margin: 0 .5rem .5rem 0;
|
margin: 0 .5rem .5rem 0;
|
||||||
box-shadow: 0 0 3px var(--darken-weak);
|
box-shadow: 0 0 3px var(--darken-weak);
|
||||||
background: var(--profile);
|
background: var(--profile);
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
display: inline-block;
|
||||||
|
width: 1px;
|
||||||
|
height: 0;
|
||||||
|
padding-bottom: 150%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.link {
|
.link {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
color: var(--text-light);
|
color: var(--text-light);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
|
||||||
|
@ -144,15 +162,18 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
.avatar-container {
|
.avatar-container {
|
||||||
|
display: flex;
|
||||||
|
flex-grow: 1;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.avatar {
|
.avatar {
|
||||||
color: var(--darken-weak);
|
color: var(--darken-weak);
|
||||||
background: var(--darken-hint);
|
background: var(--darken-hint);
|
||||||
height: 13rem;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
|
|
59
src/media.js
59
src/media.js
|
@ -334,7 +334,7 @@ async function fetchSource(source, baseMedia) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const { pathname } = new URL(source.src);
|
const { pathname } = new URL(source.src);
|
||||||
const mimetype = mime.getType(pathname);
|
const mimetype = res.headers['content-type'] || mime.getType(pathname);
|
||||||
const extension = mime.getExtension(mimetype);
|
const extension = mime.getExtension(mimetype);
|
||||||
const type = mimetype?.split('/')[0] || 'image';
|
const type = mimetype?.split('/')[0] || 'image';
|
||||||
|
|
||||||
|
@ -342,7 +342,9 @@ async function fetchSource(source, baseMedia) {
|
||||||
hasher.setEncoding('hex');
|
hasher.setEncoding('hex');
|
||||||
|
|
||||||
const hashStream = new PassThrough();
|
const hashStream = new PassThrough();
|
||||||
const metaStream = type === 'image' ? sharp() : new PassThrough();
|
const metaStream = type === 'image'
|
||||||
|
? sharp()
|
||||||
|
: new PassThrough();
|
||||||
|
|
||||||
const tempFilePath = path.join(config.media.path, 'temp', `${baseMedia.id}.${extension}`);
|
const tempFilePath = path.join(config.media.path, 'temp', `${baseMedia.id}.${extension}`);
|
||||||
const tempThumbPath = path.join(config.media.path, 'temp', `${baseMedia.id}_thumb.${extension}`);
|
const tempThumbPath = path.join(config.media.path, 'temp', `${baseMedia.id}_thumb.${extension}`);
|
||||||
|
@ -353,6 +355,8 @@ async function fetchSource(source, baseMedia) {
|
||||||
hashStream.on('data', chunk => hasher.write(chunk));
|
hashStream.on('data', chunk => hasher.write(chunk));
|
||||||
|
|
||||||
if (type === 'image') {
|
if (type === 'image') {
|
||||||
|
// generate thumbnail
|
||||||
|
/*
|
||||||
metaStream
|
metaStream
|
||||||
.clone()
|
.clone()
|
||||||
.resize({
|
.resize({
|
||||||
|
@ -362,34 +366,21 @@ async function fetchSource(source, baseMedia) {
|
||||||
.jpeg({ quality: config.media.thumbnailQuality })
|
.jpeg({ quality: config.media.thumbnailQuality })
|
||||||
.pipe(tempThumbTarget)
|
.pipe(tempThumbTarget)
|
||||||
.on('error', error => logger.error(error));
|
.on('error', error => logger.error(error));
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
// pipeline destroys streams
|
// pipeline destroys streams, so attach info event first
|
||||||
const infoPromise = type === 'image' ? once(metaStream, 'info') : Promise.resolve([{}]);
|
const infoPromise = type === 'image' ? once(metaStream, 'info') : Promise.resolve([{}]);
|
||||||
const metaPromise = type === 'image' ? metaStream.stats() : Promise.resolve();
|
const metaPromise = type === 'image' ? metaStream.stats() : Promise.resolve();
|
||||||
|
|
||||||
await pipeline(
|
await pipeline(
|
||||||
res.originalRes,
|
res.originalRes,
|
||||||
metaStream,
|
// metaStream,
|
||||||
hashStream,
|
hashStream,
|
||||||
tempFileTarget,
|
tempFileTarget,
|
||||||
);
|
);
|
||||||
|
|
||||||
/*
|
const [stats, info] = await Promise.all([metaPromise, infoPromise]);
|
||||||
res.originalRes
|
|
||||||
.pipe(metaStream)
|
|
||||||
.pipe(hashStream)
|
|
||||||
.pipe(tempFileTarget);
|
|
||||||
*/
|
|
||||||
|
|
||||||
logger.silly(`Temporarily saved media from ${source.src}`);
|
|
||||||
|
|
||||||
const [stats, info] = await Promise.all([
|
|
||||||
metaPromise,
|
|
||||||
infoPromise,
|
|
||||||
]);
|
|
||||||
|
|
||||||
logger.silly(`Ended pipeline for ${source.src}`);
|
|
||||||
|
|
||||||
hasher.end();
|
hasher.end();
|
||||||
|
|
||||||
|
@ -398,7 +389,7 @@ async function fetchSource(source, baseMedia) {
|
||||||
|
|
||||||
peakMemoryUsage = Math.max(getMemoryUsage(), peakMemoryUsage);
|
peakMemoryUsage = Math.max(getMemoryUsage(), peakMemoryUsage);
|
||||||
|
|
||||||
logger.silly(`Retrieved metadata from ${source.src}`);
|
logger.silly(`Fetched media from ${source.src}, memory usage ${peakMemoryUsage.toFixed(2)} MB`);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...source,
|
...source,
|
||||||
|
@ -422,14 +413,21 @@ async function fetchSource(source, baseMedia) {
|
||||||
|
|
||||||
if (attempts < 3) {
|
if (attempts < 3) {
|
||||||
await Promise.delay(1000);
|
await Promise.delay(1000);
|
||||||
return attempt(attempts + 1);
|
|
||||||
|
return Promise.race([
|
||||||
|
attempt(attempts + 1),
|
||||||
|
Promise.delay(120 * 1000).then(() => { throw new Error(`Media fetch attempt ${attempts}/3 timed out, aborting ${source.src}`); }),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(`Failed to fetch ${source.src}: ${error.message}`);
|
throw new Error(`Failed to fetch ${source.src}: ${error.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return attempt(1);
|
return Promise.race([
|
||||||
|
attempt(1),
|
||||||
|
Promise.delay(120 * 1000).then(() => { throw new Error(`Media fetch timed out, aborting ${source.src}`); }),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function trySource(baseSource, existingMedias, baseMedia) {
|
async function trySource(baseSource, existingMedias, baseMedia) {
|
||||||
|
@ -437,14 +435,14 @@ async function trySource(baseSource, existingMedias, baseMedia) {
|
||||||
const extractedSource = await extractSource(baseSource, existingMedias);
|
const extractedSource = await extractSource(baseSource, existingMedias);
|
||||||
const existingSourceMedia = existingMedias.existingSourceMediaByUrl[extractedSource.src];
|
const existingSourceMedia = existingMedias.existingSourceMediaByUrl[extractedSource.src];
|
||||||
|
|
||||||
if (extractedSource.entry) {
|
if (!argv.force && extractedSource.entry) {
|
||||||
logger.silly(`Media page URL already in database, not extracting ${baseSource.url}`);
|
logger.silly(`Media page URL already in database, not extracting ${baseSource.url}`);
|
||||||
|
|
||||||
// media entry found during extraction, don't fetch
|
// media entry found during extraction, don't fetch
|
||||||
return extractedSource;
|
return extractedSource;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (existingSourceMedia) {
|
if (!argv.force && existingSourceMedia) {
|
||||||
logger.silly(`Media source URL already in database, skipping ${baseSource.src}`);
|
logger.silly(`Media source URL already in database, skipping ${baseSource.src}`);
|
||||||
|
|
||||||
// media entry found by source URL, don't fetch
|
// media entry found by source URL, don't fetch
|
||||||
|
@ -461,7 +459,13 @@ async function fetchMedia(baseMedia, existingMedias) {
|
||||||
try {
|
try {
|
||||||
const source = await baseMedia.sources.reduce(
|
const source = await baseMedia.sources.reduce(
|
||||||
// try each source until success
|
// try each source until success
|
||||||
(result, baseSource, baseSourceIndex) => result.catch(async () => trySource(baseSource, existingMedias, baseMedia, baseSourceIndex)),
|
(result, baseSource, baseSourceIndex) => result.catch(async (error) => {
|
||||||
|
if (error.message) {
|
||||||
|
logger.warn(error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
return trySource(baseSource, existingMedias, baseMedia, baseSourceIndex);
|
||||||
|
}),
|
||||||
Promise.reject(new Error()),
|
Promise.reject(new Error()),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -529,7 +533,6 @@ async function storeMedias(baseMedias) {
|
||||||
const savedMedias = await Promise.map(
|
const savedMedias = await Promise.map(
|
||||||
baseMedias,
|
baseMedias,
|
||||||
async baseMedia => fetchMedia(baseMedia, { existingSourceMediaByUrl, existingExtractMediaByUrl }),
|
async baseMedia => fetchMedia(baseMedia, { existingSourceMediaByUrl, existingExtractMediaByUrl }),
|
||||||
{ concurrency: 10 },
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const [uniqueHashMedias, existingHashMedias] = await findHashDuplicates(savedMedias);
|
const [uniqueHashMedias, existingHashMedias] = await findHashDuplicates(savedMedias);
|
||||||
|
@ -599,7 +602,9 @@ async function associateReleaseMedia(releases) {
|
||||||
}, [])
|
}, [])
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
|
|
||||||
await knex.raw(`${knex(`releases_${role}`).insert(associations)} ON CONFLICT DO NOTHING`);
|
if (associations.length > 0) {
|
||||||
|
await knex.raw(`${knex(`releases_${role}`).insert(associations)} ON CONFLICT DO NOTHING`);
|
||||||
|
}
|
||||||
}, Promise.resolve());
|
}, Promise.resolve());
|
||||||
|
|
||||||
logger.debug(`Peak media fetching memory usage: ${peakMemoryUsage.toFixed(2)} MB`);
|
logger.debug(`Peak media fetching memory usage: ${peakMemoryUsage.toFixed(2)} MB`);
|
||||||
|
|
|
@ -42,6 +42,32 @@ function getAvatarFallbacks(avatar) {
|
||||||
.flat();
|
.flat();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getTrailer(scene, site, url) {
|
||||||
|
const qualities = [360, 480, 720, 1080, 2160];
|
||||||
|
|
||||||
|
const tokenRes = await post(`${site.url}/api/__record_tknreq`, {
|
||||||
|
file: scene.previewVideoUrl1080P,
|
||||||
|
sizes: qualities.join('+'),
|
||||||
|
type: 'trailer',
|
||||||
|
}, { referer: url });
|
||||||
|
|
||||||
|
if (!tokenRes.ok) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const trailerUrl = `${site.url}/api${tokenRes.body.data.url}`;
|
||||||
|
const trailersRes = await post(trailerUrl, null, { referer: url });
|
||||||
|
|
||||||
|
if (trailersRes.ok) {
|
||||||
|
return qualities.map(quality => (trailersRes.body[quality] ? {
|
||||||
|
src: trailersRes.body[quality].token,
|
||||||
|
quality,
|
||||||
|
} : null)).filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
function scrapeAll(scenes, site, origin) {
|
function scrapeAll(scenes, site, origin) {
|
||||||
return scenes.map((scene) => {
|
return scenes.map((scene) => {
|
||||||
const release = {};
|
const release = {};
|
||||||
|
@ -90,32 +116,6 @@ function scrapeUpcoming(scene, site) {
|
||||||
return [release];
|
return [release];
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getTrailer(scene, site, url) {
|
|
||||||
const qualities = [360, 480, 720, 1080, 2160];
|
|
||||||
|
|
||||||
const tokenRes = await post(`${site.url}/api/__record_tknreq`, {
|
|
||||||
file: scene.previewVideoUrl1080P,
|
|
||||||
sizes: qualities.join('+'),
|
|
||||||
type: 'trailer',
|
|
||||||
}, { referer: url });
|
|
||||||
|
|
||||||
if (!tokenRes.ok) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const trailerUrl = `${site.url}/api${tokenRes.body.data.url}`;
|
|
||||||
const trailersRes = await post(trailerUrl, null, { referer: url });
|
|
||||||
|
|
||||||
if (trailersRes.ok) {
|
|
||||||
return qualities.map(quality => (trailersRes.body[quality] ? {
|
|
||||||
src: trailersRes.body[quality].token,
|
|
||||||
quality,
|
|
||||||
} : null)).filter(Boolean);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function scrapeScene(data, url, site, baseRelease) {
|
async function scrapeScene(data, url, site, baseRelease) {
|
||||||
const scene = data.video;
|
const scene = data.video;
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue