mirror of
https://github.com/ThaUnknown/miru.git
synced 2026-04-21 04:42:05 +00:00
feat: update rss feeds periodically
This commit is contained in:
parent
f7c4c7ec6a
commit
432a4e9ccf
7 changed files with 34 additions and 233 deletions
8
jsconfig.json
Normal file
8
jsconfig.json
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": "./",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["src/renderer/*"],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -74,18 +74,24 @@ class RSSMediaManager {
|
||||||
return array[i]
|
return array[i]
|
||||||
}
|
}
|
||||||
|
|
||||||
async _getMediaForRSS (page, perPage, url) {
|
async getContentChanged (page, perPage, url) {
|
||||||
const content = await getRSSContent(getReleasesRSSurl(url))
|
const content = await getRSSContent(getReleasesRSSurl(url))
|
||||||
const pubDate = content.querySelector('pubDate').textContent * page * perPage
|
const pubDate = new Date(content.querySelector('pubDate').textContent) * page * perPage
|
||||||
if (this.resultMap[url]?.date === pubDate) return this.resultMap[url].result
|
if (this.resultMap[url]?.date === pubDate) return false
|
||||||
|
return { content, pubDate }
|
||||||
|
}
|
||||||
|
|
||||||
|
async _getMediaForRSS (page, perPage, url) {
|
||||||
|
const changed = await this.getContentChanged(page, perPage, url)
|
||||||
|
if (!changed) return this.resultMap[url].result
|
||||||
|
|
||||||
const index = (page - 1) * perPage
|
const index = (page - 1) * perPage
|
||||||
const targetPage = [...content.querySelectorAll('item')].slice(index, index + perPage)
|
const targetPage = [...changed.content.querySelectorAll('item')].slice(index, index + perPage)
|
||||||
const items = parseRSSNodes(targetPage)
|
const items = parseRSSNodes(targetPage)
|
||||||
hasNextPage.value = items.length === perPage
|
hasNextPage.value = items.length === perPage
|
||||||
const result = items.map(item => this.resolveAnimeFromRSSItem(item))
|
const result = items.map(item => this.resolveAnimeFromRSSItem(item))
|
||||||
this.resultMap[url] = {
|
this.resultMap[url] = {
|
||||||
date: pubDate,
|
date: changed.pubDate,
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ export default class Sections {
|
||||||
}
|
}
|
||||||
|
|
||||||
add (data) {
|
add (data) {
|
||||||
for (const { title, variables = {}, type, load = Sections.createFallbackLoad(variables, type), preview } of data) {
|
for (const { title, variables = {}, type, load = Sections.createFallbackLoad(variables, type), preview = writable() } of data) {
|
||||||
this.sections.push({ load, title, preview, variables })
|
this.sections.push({ load, title, preview, variables })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,222 +0,0 @@
|
||||||
<script>
|
|
||||||
import { countdown, since, wrapEnter } from '@/modules/util.js'
|
|
||||||
import { getContext } from 'svelte'
|
|
||||||
export let cards = new Promise(() => {})
|
|
||||||
const view = getContext('view')
|
|
||||||
function viewMedia (media) {
|
|
||||||
$view = media
|
|
||||||
}
|
|
||||||
export let length = 5
|
|
||||||
export let tabable = false
|
|
||||||
|
|
||||||
const statusColorMap = {
|
|
||||||
CURRENT: 'rgb(61,180,242)',
|
|
||||||
PLANNING: 'rgb(247,154,99)',
|
|
||||||
COMPLETED: 'rgb(123,213,85)',
|
|
||||||
PAUSED: 'rgb(250,122,122)',
|
|
||||||
REPEATING: '#3baeea',
|
|
||||||
DROPPED: 'rgb(232,93,117)'
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#await cards}
|
|
||||||
{#each Array(length) as _}
|
|
||||||
<div class='card m-0 p-0'>
|
|
||||||
<div class='row h-full'>
|
|
||||||
<div class='col-4 skeloader' />
|
|
||||||
<div class='col-8 bg-very-dark px-15 py-10'>
|
|
||||||
<p class='skeloader w-300 h-25 rounded bg-dark' />
|
|
||||||
<p class='skeloader w-150 h-10 rounded bg-dark' />
|
|
||||||
<p class='skeloader w-150 h-10 rounded bg-dark' />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
{:then cards}
|
|
||||||
{#each cards || [] as card}
|
|
||||||
{#if typeof card === 'string'}
|
|
||||||
<div class='day-row font-size-24 font-weight-bold h-50 d-flex align-items-end'>{card}</div>
|
|
||||||
{:else if !card.media}
|
|
||||||
<div class='card m-0 p-0' on:click={card.onclick} on:keydown={wrapEnter(card.onclick)} tabindex={tabable ? 0 : null} role='button'>
|
|
||||||
<div class='row h-full'>
|
|
||||||
<div class='col-4 skeloader' />
|
|
||||||
<div class='col-8 bg-very-dark px-15 py-10'>
|
|
||||||
<h5 class='m-0 text-capitalize font-weight-bold pb-10'>{[card.parseObject.anime_title, card.episode].filter(s => s).join(' - ')}</h5>
|
|
||||||
<p class='skeloader w-150 h-10 rounded bg-dark' />
|
|
||||||
<p class='skeloader w-150 h-10 rounded bg-dark' />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<div class='card m-0 p-0'
|
|
||||||
on:click={card.onclick || (() => viewMedia(card.media))}
|
|
||||||
on:keydown={wrapEnter(card.onclick || (() => viewMedia(card.media)))}
|
|
||||||
tabindex={tabable ? 0 : null} role='button'
|
|
||||||
style:--color={card.media.coverImage.color || '#1890ff'}
|
|
||||||
title={card.parseObject?.file_name}>
|
|
||||||
<div class='row h-full'>
|
|
||||||
<div class='col-4'>
|
|
||||||
<img loading='lazy' src={card.media.coverImage.extraLarge || ''} alt='cover' class='cover-img w-full h-full' />
|
|
||||||
</div>
|
|
||||||
<div class='col-8 h-full card-grid'>
|
|
||||||
<div class='px-15 py-10 bg-very-dark'>
|
|
||||||
<h5 class='m-0 text-capitalize font-weight-bold'>
|
|
||||||
{#if card.media.mediaListEntry?.status}
|
|
||||||
<div style:--statusColor={statusColorMap[card.media.mediaListEntry.status]} class='list-status-circle d-inline-flex overflow-hidden mr-5' title={card.media.mediaListEntry.status} />
|
|
||||||
{/if}
|
|
||||||
{#if card.failed}
|
|
||||||
<span class='badge badge-secondary'>Uncertain</span>
|
|
||||||
{/if}
|
|
||||||
{[card.media.title.userPreferred, card.episode].filter(s => s).join(' - ')}
|
|
||||||
</h5>
|
|
||||||
{#if card.schedule && card.media.nextAiringEpisode}
|
|
||||||
<span class='text-muted font-weight-bold'>
|
|
||||||
{'EP ' + card.media.nextAiringEpisode.episode + ' in ' + countdown(card.media.nextAiringEpisode.timeUntilAiring)}
|
|
||||||
</span>
|
|
||||||
{/if}
|
|
||||||
{#if card.date}
|
|
||||||
<span class='text-muted font-weight-bold'>
|
|
||||||
{since(card.date)}
|
|
||||||
</span>
|
|
||||||
{/if}
|
|
||||||
<p class='text-muted m-0 text-capitalize details'>
|
|
||||||
<span class='text-nowrap'>
|
|
||||||
{#if card.media.format === 'TV'}
|
|
||||||
TV Show
|
|
||||||
{:else if card.media.format}
|
|
||||||
{card.media.format?.toLowerCase().replace(/_/g, ' ')}
|
|
||||||
{/if}
|
|
||||||
</span>
|
|
||||||
{#if card.media.episodes && card.media.episodes !== 1}
|
|
||||||
<span class='text-nowrap'>
|
|
||||||
{#if card.media.mediaListEntry?.status === 'CURRENT' && card.media.mediaListEntry?.progress }
|
|
||||||
{card.media.mediaListEntry.progress} / {card.media.episodes} Episodes
|
|
||||||
{:else}
|
|
||||||
{card.media.episodes} Episodes
|
|
||||||
{/if}
|
|
||||||
</span>
|
|
||||||
{:else if card.media.duration}
|
|
||||||
<span class='text-nowrap'>{card.media.duration + ' Minutes'}</span>
|
|
||||||
{/if}
|
|
||||||
{#if card.media.status}
|
|
||||||
<span class='text-nowrap'>{card.media.status?.toLowerCase().replace(/_/g, ' ')}</span>
|
|
||||||
{/if}
|
|
||||||
{#if card.media.season || card.media.seasonYear}
|
|
||||||
<span class='text-nowrap'>
|
|
||||||
{[card.media.season?.toLowerCase(), card.media.seasonYear].filter(s => s).join(' ')}
|
|
||||||
</span>
|
|
||||||
{/if}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class='overflow-y-auto px-15 pb-5 bg-very-dark card-desc pre-wrap'>
|
|
||||||
{card.media.description?.replace(/<[^>]*>/g, '') || ''}
|
|
||||||
</div>
|
|
||||||
{#if card.media.genres.length}
|
|
||||||
<div class='px-15 pb-10 pt-5 genres'>
|
|
||||||
{#each card.media.genres.slice(0, 3) as genre}
|
|
||||||
<span class='badge badge-pill badge-color text-dark mt-5 mr-5 font-weight-bold'>{genre}</span>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{:else}
|
|
||||||
<div class='empty d-flex flex-column align-items-center justify-content-center'>
|
|
||||||
<h2 class='font-weight-semi-bold mb-10'>Ooops!</h2>
|
|
||||||
<div>Looks like there's nothing here.</div>
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
{/await}
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.pre-wrap {
|
|
||||||
white-space: pre-wrap
|
|
||||||
}
|
|
||||||
.empty {
|
|
||||||
height: 27rem;
|
|
||||||
grid-column: 1 / -1;
|
|
||||||
}
|
|
||||||
.h-10 {
|
|
||||||
height: 1rem !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-desc :global(p) {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.details span + span::before {
|
|
||||||
content: ' • ';
|
|
||||||
white-space: normal;
|
|
||||||
}
|
|
||||||
.card {
|
|
||||||
animation: 0.3s ease 0s 1 load-in;
|
|
||||||
cursor: pointer;
|
|
||||||
overflow: hidden;
|
|
||||||
transition: transform 0.2s ease;
|
|
||||||
height: 27rem !important;
|
|
||||||
box-shadow: rgba(0, 4, 12, 0.3) 0px 7px 15px, rgba(0, 4, 12, 0.05) 0px 4px 4px;
|
|
||||||
}
|
|
||||||
.card-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-rows: auto 1fr auto;
|
|
||||||
}
|
|
||||||
.badge-color {
|
|
||||||
background-color: var(--color) !important;
|
|
||||||
border-color: var(--color) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card:hover {
|
|
||||||
transform: scale(1.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes load {
|
|
||||||
from {
|
|
||||||
left: -100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
to {
|
|
||||||
left: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes load-in {
|
|
||||||
from {
|
|
||||||
bottom: -1.2rem;
|
|
||||||
transform: scale(0.95);
|
|
||||||
}
|
|
||||||
|
|
||||||
to {
|
|
||||||
bottom: 0;
|
|
||||||
transform: scale(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.skeloader {
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.skeloader::before {
|
|
||||||
will-change: left;
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
height: 100%;
|
|
||||||
width: 15rem;
|
|
||||||
background: linear-gradient(to right, transparent 0%, #25282c 50%, transparent 100%);
|
|
||||||
animation: load 1s infinite cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
}
|
|
||||||
.cover-img {
|
|
||||||
object-fit: cover;
|
|
||||||
background-color: var(--color) !important;
|
|
||||||
}
|
|
||||||
.day-row {
|
|
||||||
grid-column: 1 / -1;
|
|
||||||
}
|
|
||||||
.list-status-circle {
|
|
||||||
background: var(--statusColor);
|
|
||||||
height: 1.1rem;
|
|
||||||
width: 1.1rem;
|
|
||||||
border-radius: 50%;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
<script context='module'>
|
<script context='module'>
|
||||||
|
import { writable } from 'simple-store-svelte'
|
||||||
import Sections from '@/modules/sections.js'
|
import Sections from '@/modules/sections.js'
|
||||||
import { alToken, set } from '../Settings.svelte'
|
import { alToken, set } from '../Settings.svelte'
|
||||||
import { alRequest, userLists } from '@/modules/anilist.js'
|
import { alRequest, userLists } from '@/modules/anilist.js'
|
||||||
|
|
@ -14,14 +15,21 @@
|
||||||
const manager = new Sections()
|
const manager = new Sections()
|
||||||
|
|
||||||
for (const [title, url] of set.rssFeeds.reverse()) {
|
for (const [title, url] of set.rssFeeds.reverse()) {
|
||||||
|
const load = (page = 1, perPage = 6) => RSSManager.getMediaForRSS(page, perPage, url)
|
||||||
manager.add([
|
manager.add([
|
||||||
{
|
{
|
||||||
title,
|
title,
|
||||||
load: (page = 1, perPage = 6) => RSSManager.getMediaForRSS(page, perPage, url),
|
load,
|
||||||
preview: RSSManager.getMediaForRSS(1, 6, url),
|
preview: writable(RSSManager.getMediaForRSS(1, 6, url)),
|
||||||
variables: { disableSearch: true }
|
variables: { disableSearch: true }
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
const entry = manager.sections.find(section => section.load === load)
|
||||||
|
setInterval(async () => {
|
||||||
|
if (await RSSManager.getContentChanged(1, 6, url)) {
|
||||||
|
entry.preview.value = RSSManager.getMediaForRSS(1, 6, url)
|
||||||
|
}
|
||||||
|
}, 30000)
|
||||||
}
|
}
|
||||||
if (alToken) {
|
if (alToken) {
|
||||||
const sections = [
|
const sections = [
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@
|
||||||
async function deferredLoad (element) {
|
async function deferredLoad (element) {
|
||||||
const observer = new IntersectionObserver(([entry]) => {
|
const observer = new IntersectionObserver(([entry]) => {
|
||||||
if (entry.isIntersecting) {
|
if (entry.isIntersecting) {
|
||||||
if (!opts.preview) opts.preview = opts.load(1, 10)
|
if (!opts.preview.value) opts.preview.value = opts.load(1, 10)
|
||||||
observer.unobserve(element)
|
observer.unobserve(element)
|
||||||
}
|
}
|
||||||
}, { threshold: 0 })
|
}, { threshold: 0 })
|
||||||
|
|
@ -27,6 +27,7 @@
|
||||||
}
|
}
|
||||||
$page = 'search'
|
$page = 'search'
|
||||||
}
|
}
|
||||||
|
const preview = opts.preview
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<span class='d-flex px-20 align-items-end pointer text-decoration-none text-muted'
|
<span class='d-flex px-20 align-items-end pointer text-decoration-none text-muted'
|
||||||
|
|
@ -38,7 +39,7 @@
|
||||||
<div class='pr-10 ml-auto font-size-12'>View More</div>
|
<div class='pr-10 ml-auto font-size-12'>View More</div>
|
||||||
</span>
|
</span>
|
||||||
<div class='pb-10 w-full position-relative d-flex flex-row justify-content-start gallery'>
|
<div class='pb-10 w-full position-relative d-flex flex-row justify-content-start gallery'>
|
||||||
{#each opts.preview || fakecards as card}
|
{#each $preview || fakecards as card}
|
||||||
<Card {card} />
|
<Card {card} />
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -36,8 +36,8 @@
|
||||||
let canScroll = true
|
let canScroll = true
|
||||||
|
|
||||||
async function loadTillFull (element) {
|
async function loadTillFull (element) {
|
||||||
|
canScroll = false
|
||||||
while (hasNextPage.value && element.scrollHeight <= element.clientHeight) {
|
while (hasNextPage.value && element.scrollHeight <= element.clientHeight) {
|
||||||
canScroll = false
|
|
||||||
await loadSearchData()
|
await loadSearchData()
|
||||||
}
|
}
|
||||||
canScroll = true
|
canScroll = true
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue