faster n stuff

main
Gabe Farrell 1 year ago
parent 8e2300bf53
commit 9938dcc3bc

@ -1,16 +1,20 @@
<script setup lang="ts">
import { RouterView } from 'vue-router'
import { ref } from 'vue'
import JF from './jfapi'
import jfapi from './jfapi'
const menuOpen = ref(false)
const logout = () => {
JF.logout()
jfapi.Logout().then(() => {
localStorage.removeItem('jf_views')
localStorage.removeItem('jf_userid')
localStorage.removeItem('jf_host')
window.location.href = '/login'
})
}
</script>
<template>
<div class="primary-nav no-select" v-if="!$route.meta.hideNavbar">
<div class="primary-nav no-select" v-if="!$route.meta.isLoginPage">
<div class="menu-toggle" @click="menuOpen = !menuOpen">
<font-awesome-icon icon="fa-solid fa-bars" size="xl" />
</div>
@ -23,12 +27,12 @@ const logout = () => {
<div class="wrapper">
<RouterView />
</div>
<div class="footer"></div>
</template>
<style scoped>
.wrapper {
max-width: 90%;
margin-left: auto;
margin: 0 auto;
}
.primary-nav {
@ -64,4 +68,7 @@ const logout = () => {
color: var(--color-text-faded);
cursor: pointer;
}
.footer {
height: 100px;
}
</style>

@ -73,7 +73,7 @@ body {
min-height: 100vh;
min-width: 1600px;
color: var(--color-text);
background: linear-gradient(var(--color-background), var(--color-background-darker) 50%);
background-color: var(--color-background-darker);
transition:
color 0.5s,
background-color 0.5s;

@ -1,52 +0,0 @@
<script setup lang="ts">
import MediaScroller from './MediaScroller.vue'
import { ContinueWatching } from '@/feed'
import type { FeedItem } from '@/feed'
const Items = (): FeedItem[] => {
// const feed = []
// jfapi
// .GetNextUp('http://192.168.0.63:30013')
// .then((data) => {
// for (const item of data.Items) {
// const thumbtag = item.ImageTags.Thumb ? item.ImageTags.Thumb : ''
// let summary = ''
// jfapi.GetItem('http://192.168.0.63:30013', '', item.Id).then((item) => {
// summary = item.Overview !== undefined ? item.Overview : ''
// })
// feed.push({
// title: item.SeriesName,
// subtext: `S${item.ParentIndexNumber}:E${item.IndexNumber} - ${item.Name}`,
// image: jfapi.ThumbImageUrl('http://192.168.0.63:30013', item.Id, thumbtag),
// summary: summary,
// infoSubtext: `S${item.ParentIndexNumber}:E${item.IndexNumber} - ${item.Name}`,
// imdbRating: item.CommunityRating,
// runtime: getDisplayDuration(item.RunTimeTicks),
// date: item.ProductionYear,
// MPAA: item.OfficialRating,
// })
// }
// return feed
// })
// .catch((err) => {
// return err
// })
return ContinueWatching
}
// function getDisplayDuration(ticks: number) {
// const totalMinutes = Math.round(ticks / 600000000) || 1
// const totalHours = Math.floor(totalMinutes / 60)
// const remainderMinutes = totalMinutes % 60
// const result = []
// if (totalHours > 0) {
// result.push(`${totalHours}h`)
// }
// result.push(`${remainderMinutes}m`)
// return result.join(' ')
// }
</script>
<template>
<MediaScroller title="Continue Watching" landscape :items="Items()" />
</template>

@ -0,0 +1,87 @@
<script setup lang="ts">
import jfapi from '@/jfapi'
import MediaScroller from './MediaScroller.vue'
import type { FeedItem } from '@/feed'
import type { Item, View } from '@/jfapi'
const props = defineProps<{
view: View
}>()
const Items = async (): Promise<FeedItem[]> => {
return new Promise(async (resolve) => {
const feed: FeedItem[] = []
const userid = localStorage.getItem('jf_userid')
const data = await jfapi.GetLatest(userid ? userid : '', props.view)
if (data !== null) {
console.log(data)
for (const item of data) {
const summary = ''
if (userid === null) {
window.location.href = '/login'
}
// const fullitem = await jfapi.GetItem(userid, item.Id)
// if (fullitem === null) {
// continue
// } else {
// summary = fullitem.Overview
// }
feed.push({
title: item.SeriesName || item.Name,
subtext: item.SeriesName
? `S${item.ParentIndexNumber}:E${item.IndexNumber} - ${item.Name}`
: String(item.ProductionYear),
image: getImageLink(item),
summary: summary,
// infoSubtext: item.SeriesName
// ? `S${item.ParentIndexNumber}:E${item.IndexNumber} - ${item.Name}`
// : fullitem.OriginalTitle != item.Name
// ? fullitem.OriginalTitle
// : '',
infoSubtext: '',
imdbRating: String(item.CommunityRating),
runtime: getDisplayDuration(item.RunTimeTicks),
date: String(item.ProductionYear),
MPAA: item.OfficialRating,
tag:
item.UserData?.UnplayedItemCount !== undefined
? String(item.UserData.UnplayedItemCount)
: '',
itemId: item.Id,
})
}
console.log('LatestMedia.vue =>', feed)
resolve(feed)
}
})
}
function getImageLink(item: Item): string {
if (item.SeriesId === undefined) {
// using movie primary
return jfapi.PrimaryImageUrl(item.Id, item.ImageTags.Primary ? item.ImageTags.Primary : '')
} else {
// using series primary
return jfapi.PrimaryImageUrl(
item.SeriesId,
item.SeriesPrimaryImageTag ? item.SeriesPrimaryImageTag[0] : '',
)
}
}
function getDisplayDuration(ticks: number) {
const totalMinutes = Math.round(ticks / 600000000) || 1
const totalHours = Math.floor(totalMinutes / 60)
const remainderMinutes = totalMinutes % 60
const result = []
if (totalHours > 0) {
result.push(`${totalHours}h`)
}
result.push(`${remainderMinutes}m`)
return result.join(' ')
}
</script>
<template>
<MediaScroller :title="`Latest ${view?.Name}`" :feed-items="Items()" />
</template>

@ -2,21 +2,74 @@
import ScrollerItemPortrait from './ScrollerItemPortrait.vue'
import ScrollerItemLandscape from './ScrollerItemLandscape.vue'
import type { FeedItem } from '@/feed'
import { ref, onBeforeMount } from 'vue'
const props = defineProps<{
title: string
landscape?: boolean
items: FeedItem[]
feedItems: Promise<FeedItem[]>
}>()
const items = ref<FeedItem[]>()
const overflow = ref(false)
const translate = ref(0)
const vw = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0)
const renderSpace = vw * 0.55
onBeforeMount(async () => {
items.value = await props.feedItems
console.log('MediaScroller.vue =>', props.feedItems)
const itemLimit = renderSpace / (props.landscape ? 245 : 125)
console.log(props.title, '=>', itemLimit)
if (items.value.length > itemLimit) {
overflow.value = true
}
})
const translateForward = () => {
disableForward()
if (props.landscape) {
translate.value -= 1008
} else {
translate.value -= 924
}
}
const translateBackward = () => {
if (props.landscape) {
translate.value += 1008
} else {
translate.value += 924
}
}
const disableForward = () => {
const v =
(items.value ? items.value.length : 0) * (props.landscape ? 245 : 125) -
Math.abs(translate.value) <
renderSpace
return v
}
</script>
<template>
<div class="wrapper">
<div class="wrapper" v-show="(items ? items.length : 0) > 0">
<div class="scroller-header">
<div class="scroller-title">{{ props.title }}</div>
<div class="scroller-content no-select" :style="{ height: `${props.landscape ? 210 : 240}px` }">
<div class="scroller-wrapper" v-if="landscape">
<div class="scroller-controls" v-show="overflow">
<button class="translate-button" @click="translateBackward()" :disabled="translate >= 0">
<font-awesome-icon icon="fa-solid fa-play" class="fa-flip-horizontal" size="sm" />
</button>
<button class="translate-button" @click="translateForward()" :disabled="disableForward()">
<font-awesome-icon icon="fa-solid fa-play" size="sm" />
</button>
</div>
</div>
<div
class="scroller-content no-select"
:style="{
height: `${props.landscape ? 210 : 240}px`,
transform: `translateX(${translate}px)`,
}"
>
<div class="scroller-wrapper clearfix" v-if="landscape">
<ScrollerItemLandscape
v-for="item in props.items"
v-for="item in items"
:title="item.title"
:subtext="item.subtext"
:image="item.image"
@ -26,12 +79,14 @@ const props = defineProps<{
:summary="item.summary"
:-m-p-a-a="item.MPAA"
:imdb-rating="item.imdbRating"
:itemId="item.itemId"
:key="item.title"
class="scroller-item"
/>
</div>
<div class="scroller-wrapper" v-else>
<div class="scroller-wrapper clearfix" v-else>
<ScrollerItemPortrait
v-for="item in props.items"
v-for="item in items"
:title="item.title"
:subtext="item.subtext"
:image="item.image"
@ -41,8 +96,10 @@ const props = defineProps<{
:summary="item.summary"
:-m-p-a-a="item.MPAA"
:imdb-rating="item.imdbRating"
:tag="item.tag"
:key="item.title"
:itemId="item.itemId"
:tag="item.tag"
class="scroller-item"
/>
</div>
</div>
@ -58,4 +115,40 @@ const props = defineProps<{
gap: 1em;
font-size: 12px;
}
.scroller-controls svg {
padding: 1em;
margin-right: 2em;
}
.scroller-controls {
margin-right: 10em;
}
.scroller-header {
display: flex;
align-items: center;
width: 100%;
justify-content: space-between;
}
.scroller-content {
transition: all 0.3s ease;
}
.translate-button {
border: none;
background: none;
color: var(--color-text);
}
.translate-button:hover {
cursor: pointer;
color: var(--color-text-faded);
}
.translate-button:disabled {
border: none;
background: none;
color: var(--color-text-faded);
}
.translate-button:disabled:hover {
cursor: default;
}
.wrapper {
overflow: hidden;
}
</style>

@ -56,6 +56,7 @@
display: flex;
justify-content: space-between;
align-items: end;
background: linear-gradient(var(--color-background), var(--color-background-darker));
}
.media-showcase-logo img {
max-height: 90px;
@ -68,7 +69,7 @@
.media-showcase-info {
text-align: center;
padding: 3em 3em;
margin-left: 2%;
margin-left: 12%;
display: flex;
flex-direction: column;
align-items: center;

@ -0,0 +1,75 @@
<script setup lang="ts">
import jfapi from '@/jfapi'
import MediaScroller from './MediaScroller.vue'
import type { FeedItem } from '@/feed'
import type { Item } from '@/jfapi'
const Items = async (): Promise<FeedItem[]> => {
return new Promise(async (resolve) => {
const feed: FeedItem[] = []
const data = await jfapi.GetNextUp()
if (data !== null) {
for (const item of data.Items) {
const summary = ''
const userid = localStorage.getItem('jf_userid')
if (userid === null) {
window.location.href = '/login'
}
// const fullitem = await jfapi.GetItem(userid ? userid : '', item.Id)
// if (fullitem === null) {
// summary = 'Failed to fetch content.'
// } else {
// summary = fullitem.Overview
// }
feed.push({
title: item.SeriesName !== undefined ? item.SeriesName : '',
subtext: `S${item.ParentIndexNumber}:E${item.IndexNumber} - ${item.Name}`,
image: getImageLink(item),
summary: summary,
infoSubtext: `S${item.ParentIndexNumber}:E${item.IndexNumber} - ${item.Name}`,
imdbRating: String(item.CommunityRating),
runtime: getDisplayDuration(item.RunTimeTicks),
date: String(item.ProductionYear),
MPAA: item.OfficialRating,
itemId: item.Id,
tag: '',
})
}
console.log('NextUp.vue =>', feed)
resolve(feed)
}
})
}
function getImageLink(item: Item): string {
if (item.ParentThumbItemId === undefined) {
// using backdrop
return jfapi.BackdropImageUrl(
item.ParentBackdropItemId ? item.ParentBackdropItemId : '',
item.ParentBackdropImageTags ? item.ParentBackdropImageTags[0] : '',
)
} else {
// using thumb
return jfapi.ThumbImageUrl(
item.ParentThumbItemId,
item.ParentThumbImageTag ? item.ParentThumbImageTag : '',
)
}
}
function getDisplayDuration(ticks: number) {
const totalMinutes = Math.round(ticks / 600000000) || 1
const totalHours = Math.floor(totalMinutes / 60)
const remainderMinutes = totalMinutes % 60
const result = []
if (totalHours > 0) {
result.push(`${totalHours}h`)
}
result.push(`${remainderMinutes}m`)
return result.join(' ')
}
</script>
<template>
<MediaScroller title="Next Up" landscape :feed-items="Items()" />
</template>

@ -1,4 +1,6 @@
<script setup lang="ts">
import jfapi from '@/jfapi'
import { ref } from 'vue'
const props = defineProps({
title: {
type: String,
@ -34,15 +36,33 @@ const props = defineProps({
MPAA: {
type: String,
},
itemId: {
type: String,
required: true,
},
})
const overview = ref('')
const userid = localStorage.getItem('jf_userid')
jfapi.GetItem(userid ? userid : '', props.itemId).then((data) => {
if (data === null) {
// error
} else {
overview.value = data.Overview
}
})
</script>
<template>
<div class="scroller-item">
<div
class="scroller-item"
:style="{
width: `${240}px`,
}"
>
<div class="scroller-item-img">
<font-awesome-icon icon="fa-solid fa-play" class="play-icon" />
<img
:src="'/src/assets/images/' + props.image"
:src="props.image"
alt=""
:style="{
width: `${240}px`,
@ -54,7 +74,7 @@ const props = defineProps({
<div class="info-box-title">{{ props.title }}</div>
<div class="info-box-subtext">{{ props.infoSubtext }}</div>
</div>
<div class="info-box-summary clamp-4">{{ props.summary }}</div>
<div class="info-box-summary clamp-4">{{ overview }}</div>
<div class="info-box-misc-info">
<div class="date">{{ props.date }}</div>
<div class="runtime">{{ props.runtime }}</div>
@ -62,7 +82,7 @@ const props = defineProps({
<font-awesome-icon icon="fa-solid fa-star" size="xs" class="star-rating" />
{{ props.imdbRating }}
</div>
<div class="mpaa">
<div class="mpaa" v-show="props.MPAA !== undefined">
<span class="age-rating sm"> {{ props.MPAA }}</span>
</div>
<div class="ends-at">Ends at 6:23pm</div>
@ -183,7 +203,7 @@ const props = defineProps({
gap: 1em;
min-height: 100%;
}
.scroller-item:hover .info-box-content {
.scroller-item-img:hover .info-box-content {
max-height: 183px;
transition-delay: 0.05s;
}

@ -1,4 +1,7 @@
<script setup lang="ts">
import jfapi from '@/jfapi'
import { ref } from 'vue'
const props = defineProps({
title: {
type: String,
@ -37,6 +40,26 @@ const props = defineProps({
tag: {
type: String,
},
itemId: {
type: String,
required: true,
},
})
const overview = ref('')
const infosubtxt = ref('')
const userid = localStorage.getItem('jf_userid')
jfapi.GetItem(userid ? userid : '', props.itemId).then((data) => {
if (data === null) {
// error
} else {
overview.value = data.Overview
infosubtxt.value =
data.Type === 'Episode'
? `S${data.ParentIndexNumber}:E${data.IndexNumber} - ${data.Name}`
: data.OriginalTitle != data.Name
? data.OriginalTitle
: ''
}
})
</script>
@ -49,7 +72,7 @@ const props = defineProps({
</div>
<font-awesome-icon icon="fa-solid fa-play" class="play-icon" />
<img
:src="'/src/assets/images/' + props.image"
:src="props.image"
alt=""
:style="{
width: `${120}px`,
@ -60,10 +83,12 @@ const props = defineProps({
<div class="info-box">
<div class="info-box-content">
<div class="info-box-header">
<div class="info-box-title">{{ props.title }}</div>
<div class="info-box-subtext">{{ props.infoSubtext }}</div>
<div class="info-box-title clamp">{{ props.title }}</div>
<div class="info-box-subtext" v-show="infosubtxt !== ''">
{{ infosubtxt }}
</div>
</div>
<div class="info-box-summary clamp-6">{{ props.summary }}</div>
<div class="info-box-summary clamp-6">{{ overview }}</div>
<div class="info-box-misc-info">
<div class="date">{{ props.date }}</div>
<div class="runtime">{{ props.runtime }}</div>
@ -71,7 +96,7 @@ const props = defineProps({
<font-awesome-icon icon="fa-solid fa-star" size="xs" class="star-rating" />
{{ props.imdbRating }}
</div>
<div class="mpaa">
<div class="mpaa" v-show="props.MPAA !== undefined">
<span class="age-rating sm">{{ props.MPAA }}</span>
</div>
<div class="ends-at">Ends at 6:23pm</div>
@ -164,6 +189,7 @@ const props = defineProps({
backdrop-filter: blur(2.6px);
-webkit-backdrop-filter: blur(2.6px);
border: 1px solid rgba(255, 255, 255, 0.2);
border-right: none;
text-align: center;
transition: all var(--hover-animation-speed) ease 0.2s;
opacity: 100%;
@ -216,7 +242,7 @@ const props = defineProps({
gap: 2em;
min-height: 95%;
}
.scroller-item:hover .info-box-content {
.scroller-item-img:hover .info-box-content {
max-height: 240px;
transition-delay: 0.05s;
}

@ -1,53 +1,53 @@
const ContinueWatching: FeedItem[] = [
{
title: 'Bloom Into You',
subtext: 'S1:E7 - Secrets Galore / Sparks',
image: 'biy_ep.jpg',
summary: `Hee-do takes one giant leap closer to her dream. She's eager to tell Yi-jin the good news — but realizes he's disappeared without a trace. This is more content than there was before.`,
infoSubtext: 'S1:E5 - Episode 5',
imdbRating: '8.7',
runtime: '1h 16m',
date: '2022',
MPAA: 'TV-14',
tag: '',
},
{
title: 'The Bear',
subtext: 'S3:E9 - Apologies',
image: 'bear_ep.jpg',
summary: `Hee-do takes one giant leap closer to her dream. She's eager to tell Yi-jin the good news — but realizes he's disappeared without a trace.`,
infoSubtext: 'S1:E5 - Episode 5',
imdbRating: '8.7',
runtime: '1h 16m',
date: '2022',
MPAA: 'TV-14',
tag: '',
},
{
title: 'BOCCHI THE ROCK!',
subtext: 'S1:E12 - Morning Light Falls on You',
image: 'bocchi_ep.jpg',
summary: `Hee-do takes one giant leap closer to her dream. She's eager to tell Yi-jin the good news — but realizes he's disappeared without a trace.`,
infoSubtext: 'S1:E5 - Episode 5',
imdbRating: '8.7',
runtime: '1h 16m',
date: '2022',
MPAA: 'TV-14',
tag: '',
},
{
title: 'Twenty Five Twenty One',
subtext: 'S1:E5 - Episode 5',
image: '2521_ep.jpg',
summary: `Hee-do takes one giant leap closer to her dream. She's eager to tell Yi-jin the good news — but realizes he's disappeared without a trace.`,
infoSubtext: 'S1:E5 - Episode 5',
imdbRating: '8.7',
runtime: '1h 16m',
date: '2022',
MPAA: 'TV-14',
tag: '',
},
]
// const ContinueWatching: FeedItem[] = [
// {
// title: 'Bloom Into You',
// subtext: 'S1:E7 - Secrets Galore / Sparks',
// image: 'biy_ep.jpg',
// summary: `Hee-do takes one giant leap closer to her dream. She's eager to tell Yi-jin the good news — but realizes he's disappeared without a trace. This is more content than there was before.`,
// infoSubtext: 'S1:E5 - Episode 5',
// imdbRating: '8.7',
// runtime: '1h 16m',
// date: '2022',
// MPAA: 'TV-14',
// tag: '',
// },
// {
// title: 'The Bear',
// subtext: 'S3:E9 - Apologies',
// image: 'bear_ep.jpg',
// summary: `Hee-do takes one giant leap closer to her dream. She's eager to tell Yi-jin the good news — but realizes he's disappeared without a trace.`,
// infoSubtext: 'S1:E5 - Episode 5',
// imdbRating: '8.7',
// runtime: '1h 16m',
// date: '2022',
// MPAA: 'TV-14',
// tag: '',
// },
// {
// title: 'BOCCHI THE ROCK!',
// subtext: 'S1:E12 - Morning Light Falls on You',
// image: 'bocchi_ep.jpg',
// summary: `Hee-do takes one giant leap closer to her dream. She's eager to tell Yi-jin the good news — but realizes he's disappeared without a trace.`,
// infoSubtext: 'S1:E5 - Episode 5',
// imdbRating: '8.7',
// runtime: '1h 16m',
// date: '2022',
// MPAA: 'TV-14',
// tag: '',
// },
// {
// title: 'Twenty Five Twenty One',
// subtext: 'S1:E5 - Episode 5',
// image: '2521_ep.jpg',
// summary: `Hee-do takes one giant leap closer to her dream. She's eager to tell Yi-jin the good news — but realizes he's disappeared without a trace.`,
// infoSubtext: 'S1:E5 - Episode 5',
// imdbRating: '8.7',
// runtime: '1h 16m',
// date: '2022',
// MPAA: 'TV-14',
// tag: '',
// },
// ]
type FeedItem = {
title: string
@ -60,265 +60,266 @@ type FeedItem = {
date: string
MPAA: string
tag: string
itemId: string
}
const LatestMovies: FeedItem[] = [
{
title: 'Alien',
subtext: '1979',
image: 'alien.jpg',
tag: '',
summary:
'The films spans two decades as the story unfolds in a series of flashbacks that begin when Qiyue and Ansheng were just thirteen. The two became inseparable until they met a boy who ended up tearing their lives apart.',
infoSubtext: '七月与安生',
imdbRating: '7.3',
runtime: '1h 50m',
date: '2016',
MPAA: 'PG-13',
},
{
title: 'Your Name',
subtext: '2017',
image: 'yourname.jpg',
tag: '',
summary:
'The films spans two decades as the story unfolds in a series of flashbacks that begin when Qiyue and Ansheng were just thirteen. The two became inseparable until they met a boy who ended up tearing their lives apart.',
infoSubtext: '七月与安生',
imdbRating: '7.3',
runtime: '1h 50m',
date: '2016',
MPAA: 'PG-13',
},
{
title: 'SoulMate',
subtext: '2016',
image: 'soulmate.jpg',
summary:
'The films spans two decades as the story unfolds in a series of flashbacks that begin when Qiyue and Ansheng were just thirteen. The two became inseparable until they met a boy who ended up tearing their lives apart.',
infoSubtext: '七月与安生',
imdbRating: '7.3',
runtime: '1h 50m',
date: '2016',
MPAA: 'PG-13',
tag: '',
},
{
title: 'Burning',
subtext: '2018',
image: 'burning.jpg',
tag: '',
summary:
'The films spans two decades as the story unfolds in a series of flashbacks that begin when Qiyue and Ansheng were just thirteen. The two became inseparable until they met a boy who ended up tearing their lives apart.',
infoSubtext: '七月与安生',
imdbRating: '7.3',
runtime: '1h 50m',
date: '2016',
MPAA: 'PG-13',
},
{
title: 'Better Days',
subtext: '2019',
image: 'betterdays.jpg',
tag: '',
summary:
'The films spans two decades as the story unfolds in a series of flashbacks that begin when Qiyue and Ansheng were just thirteen. The two became inseparable until they met a boy who ended up tearing their lives apart.',
infoSubtext: '七月与安生',
imdbRating: '7.3',
runtime: '1h 50m',
date: '2016',
MPAA: 'PG-13',
},
{
title: 'House of Hummingbird',
subtext: '2019',
image: 'hummingbird.jpg',
tag: '',
summary:
'The films spans two decades as the story unfolds in a series of flashbacks that begin when Qiyue and Ansheng were just thirteen. The two became inseparable until they met a boy who ended up tearing their lives apart.',
infoSubtext: '七月与安生',
imdbRating: '7.3',
runtime: '1h 50m',
date: '2016',
MPAA: 'PG-13',
},
{
title: 'Where We Belong',
subtext: '2019',
image: 'wwb.jpg',
summary: `Will there be someone like you where i'm going?\n
A teenager creates a checklist to complete before she studies abroad and realizes that her toughest task is leaving behind her best friend.`,
infoSubtext: 'ที่ตรงนั้น มีฉันหรือเปล่า',
imdbRating: '7.6',
runtime: '2h 12m',
date: '2019',
MPAA: 'R',
tag: '',
},
{
title: '1917',
subtext: '2019',
image: '1917.jpg',
tag: '',
summary:
'The films spans two decades as the story unfolds in a series of flashbacks that begin when Qiyue and Ansheng were just thirteen. The two became inseparable until they met a boy who ended up tearing their lives apart.',
infoSubtext: '七月与安生',
imdbRating: '7.3',
runtime: '1h 50m',
date: '2016',
MPAA: 'PG-13',
},
{
title: 'Life of Brian',
subtext: '1979',
image: 'lob.jpg',
tag: '',
summary:
'The films spans two decades as the story unfolds in a series of flashbacks that begin when Qiyue and Ansheng were just thirteen. The two became inseparable until they met a boy who ended up tearing their lives apart.',
infoSubtext: '七月与安生',
imdbRating: '7.3',
runtime: '1h 50m',
date: '2016',
MPAA: 'PG-13',
},
{
title: 'Shoplifters',
subtext: '2018',
image: 'shoplifters.jpg',
tag: '',
summary:
'The films spans two decades as the story unfolds in a series of flashbacks that begin when Qiyue and Ansheng were just thirteen. The two became inseparable until they met a boy who ended up tearing their lives apart.',
infoSubtext: '七月与安生',
imdbRating: '7.3',
runtime: '1h 50m',
date: '2016',
MPAA: 'PG-13',
},
{
title: 'The Lobster',
subtext: '2016',
image: 'lobster.jpg',
tag: '',
summary:
'The films spans two decades as the story unfolds in a series of flashbacks that begin when Qiyue and Ansheng were just thirteen. The two became inseparable until they met a boy who ended up tearing their lives apart.',
infoSubtext: '七月与安生',
imdbRating: '7.3',
runtime: '1h 50m',
date: '2016',
MPAA: 'PG-13',
},
{
title: 'Her',
subtext: '2013',
image: 'her.jpg',
tag: '',
summary:
'The films spans two decades as the story unfolds in a series of flashbacks that begin when Qiyue and Ansheng were just thirteen. The two became inseparable until they met a boy who ended up tearing their lives apart.',
infoSubtext: '七月与安生',
imdbRating: '7.3',
runtime: '1h 50m',
date: '2016',
MPAA: 'PG-13',
},
]
// const LatestMovies: FeedItem[] = [
// {
// title: 'Alien',
// subtext: '1979',
// image: 'alien.jpg',
// tag: '',
// summary:
// 'The films spans two decades as the story unfolds in a series of flashbacks that begin when Qiyue and Ansheng were just thirteen. The two became inseparable until they met a boy who ended up tearing their lives apart.',
// infoSubtext: '七月与安生',
// imdbRating: '7.3',
// runtime: '1h 50m',
// date: '2016',
// MPAA: 'PG-13',
// },
// {
// title: 'Your Name',
// subtext: '2017',
// image: 'yourname.jpg',
// tag: '',
// summary:
// 'The films spans two decades as the story unfolds in a series of flashbacks that begin when Qiyue and Ansheng were just thirteen. The two became inseparable until they met a boy who ended up tearing their lives apart.',
// infoSubtext: '七月与安生',
// imdbRating: '7.3',
// runtime: '1h 50m',
// date: '2016',
// MPAA: 'PG-13',
// },
// {
// title: 'SoulMate',
// subtext: '2016',
// image: 'soulmate.jpg',
// summary:
// 'The films spans two decades as the story unfolds in a series of flashbacks that begin when Qiyue and Ansheng were just thirteen. The two became inseparable until they met a boy who ended up tearing their lives apart.',
// infoSubtext: '七月与安生',
// imdbRating: '7.3',
// runtime: '1h 50m',
// date: '2016',
// MPAA: 'PG-13',
// tag: '',
// },
// {
// title: 'Burning',
// subtext: '2018',
// image: 'burning.jpg',
// tag: '',
// summary:
// 'The films spans two decades as the story unfolds in a series of flashbacks that begin when Qiyue and Ansheng were just thirteen. The two became inseparable until they met a boy who ended up tearing their lives apart.',
// infoSubtext: '七月与安生',
// imdbRating: '7.3',
// runtime: '1h 50m',
// date: '2016',
// MPAA: 'PG-13',
// },
// {
// title: 'Better Days',
// subtext: '2019',
// image: 'betterdays.jpg',
// tag: '',
// summary:
// 'The films spans two decades as the story unfolds in a series of flashbacks that begin when Qiyue and Ansheng were just thirteen. The two became inseparable until they met a boy who ended up tearing their lives apart.',
// infoSubtext: '七月与安生',
// imdbRating: '7.3',
// runtime: '1h 50m',
// date: '2016',
// MPAA: 'PG-13',
// },
// {
// title: 'House of Hummingbird',
// subtext: '2019',
// image: 'hummingbird.jpg',
// tag: '',
// summary:
// 'The films spans two decades as the story unfolds in a series of flashbacks that begin when Qiyue and Ansheng were just thirteen. The two became inseparable until they met a boy who ended up tearing their lives apart.',
// infoSubtext: '七月与安生',
// imdbRating: '7.3',
// runtime: '1h 50m',
// date: '2016',
// MPAA: 'PG-13',
// },
// {
// title: 'Where We Belong',
// subtext: '2019',
// image: 'wwb.jpg',
// summary: `Will there be someone like you where i'm going?\n
// A teenager creates a checklist to complete before she studies abroad and realizes that her toughest task is leaving behind her best friend.`,
// infoSubtext: 'ที่ตรงนั้น มีฉันหรือเปล่า',
// imdbRating: '7.6',
// runtime: '2h 12m',
// date: '2019',
// MPAA: 'R',
// tag: '',
// },
// {
// title: '1917',
// subtext: '2019',
// image: '1917.jpg',
// tag: '',
// summary:
// 'The films spans two decades as the story unfolds in a series of flashbacks that begin when Qiyue and Ansheng were just thirteen. The two became inseparable until they met a boy who ended up tearing their lives apart.',
// infoSubtext: '七月与安生',
// imdbRating: '7.3',
// runtime: '1h 50m',
// date: '2016',
// MPAA: 'PG-13',
// },
// {
// title: 'Life of Brian',
// subtext: '1979',
// image: 'lob.jpg',
// tag: '',
// summary:
// 'The films spans two decades as the story unfolds in a series of flashbacks that begin when Qiyue and Ansheng were just thirteen. The two became inseparable until they met a boy who ended up tearing their lives apart.',
// infoSubtext: '七月与安生',
// imdbRating: '7.3',
// runtime: '1h 50m',
// date: '2016',
// MPAA: 'PG-13',
// },
// {
// title: 'Shoplifters',
// subtext: '2018',
// image: 'shoplifters.jpg',
// tag: '',
// summary:
// 'The films spans two decades as the story unfolds in a series of flashbacks that begin when Qiyue and Ansheng were just thirteen. The two became inseparable until they met a boy who ended up tearing their lives apart.',
// infoSubtext: '七月与安生',
// imdbRating: '7.3',
// runtime: '1h 50m',
// date: '2016',
// MPAA: 'PG-13',
// },
// {
// title: 'The Lobster',
// subtext: '2016',
// image: 'lobster.jpg',
// tag: '',
// summary:
// 'The films spans two decades as the story unfolds in a series of flashbacks that begin when Qiyue and Ansheng were just thirteen. The two became inseparable until they met a boy who ended up tearing their lives apart.',
// infoSubtext: '七月与安生',
// imdbRating: '7.3',
// runtime: '1h 50m',
// date: '2016',
// MPAA: 'PG-13',
// },
// {
// title: 'Her',
// subtext: '2013',
// image: 'her.jpg',
// tag: '',
// summary:
// 'The films spans two decades as the story unfolds in a series of flashbacks that begin when Qiyue and Ansheng were just thirteen. The two became inseparable until they met a boy who ended up tearing their lives apart.',
// infoSubtext: '七月与安生',
// imdbRating: '7.3',
// runtime: '1h 50m',
// date: '2016',
// MPAA: 'PG-13',
// },
// ]
const LatestAnime: FeedItem[] = [
{
title: 'Re:Zero - Starting Life in Another World',
subtext: 'S3:E6 - Conditions of the Life We Live In',
image: 'rezero.jpg',
summary: `Hee-do takes one giant leap closer to her dream. She's eager to tell Yi-jin the good news — but realizes he's disappeared without a trace.`,
infoSubtext: 'S1:E5 - Episode 5',
imdbRating: '8.7',
runtime: '1h 16m',
date: '2022',
MPAA: 'TV-14',
tag: '',
},
{
title: 'Serial Experiments Lain',
subtext: '1998',
image: 'lain.jpg',
tag: '8',
summary: `Hee-do takes one giant leap closer to her dream. She's eager to tell Yi-jin the good news — but realizes he's disappeared without a trace.`,
infoSubtext: 'S1:E5 - Episode 5',
imdbRating: '8.7',
runtime: '1h 16m',
date: '2022',
MPAA: 'TV-14',
},
{
title: 'Monster',
subtext: '2004-2005',
image: 'monster.jpg',
summary: `Hee-do takes one giant leap closer to her dream. She's eager to tell Yi-jin the good news — but realizes he's disappeared without a trace.`,
infoSubtext: 'S1:E5 - Episode 5',
imdbRating: '8.7',
runtime: '1h 16m',
date: '2022',
MPAA: 'TV-14',
tag: '',
},
{
title: 'Bloom Into You',
subtext: '2018',
image: 'biy.jpg',
tag: '13',
summary: `Hee-do takes one giant leap closer to her dream. She's eager to tell Yi-jin the good news — but realizes he's disappeared without a trace.`,
infoSubtext: 'S1:E5 - Episode 5',
imdbRating: '8.7',
runtime: '1h 16m',
date: '2022',
MPAA: 'TV-14',
},
{
title: 'Attack on Titan',
subtext: '2013-2022',
image: 'aot.jpg',
tag: '99+',
summary: `Hee-do takes one giant leap closer to her dream. She's eager to tell Yi-jin the good news — but realizes he's disappeared without a trace.`,
infoSubtext: 'S1:E5 - Episode 5',
imdbRating: '8.7',
runtime: '1h 16m',
date: '2022',
MPAA: 'TV-14',
},
{
title: "Frieren: Beyond Journey's End",
subtext: '2023-2024',
image: 'frieren.jpg',
summary: `Hee-do takes one giant leap closer to her dream. She's eager to tell Yi-jin the good news — but realizes he's disappeared without a trace.`,
infoSubtext: 'S1:E5 - Episode 5',
imdbRating: '8.7',
runtime: '1h 16m',
date: '2022',
MPAA: 'TV-14',
tag: '',
},
{
title: 'Lycoris Recoil',
subtext: '2022',
image: 'lycoris.jpg',
summary: `Hee-do takes one giant leap closer to her dream. She's eager to tell Yi-jin the good news — but realizes he's disappeared without a trace.`,
infoSubtext: 'S1:E5 - Episode 5',
imdbRating: '8.7',
runtime: '1h 16m',
date: '2022',
MPAA: 'TV-14',
tag: '',
},
{
title: 'Tengoku Daimakyo',
subtext: '2023 - Present',
image: 'tengoku.jpg',
summary: `Hee-do takes one giant leap closer to her dream. She's eager to tell Yi-jin the good news — but realizes he's disappeared without a trace.`,
infoSubtext: 'S1:E5 - Episode 5',
imdbRating: '8.7',
runtime: '1h 16m',
date: '2022',
MPAA: 'TV-14',
tag: '',
},
]
// const LatestAnime: FeedItem[] = [
// {
// title: 'Re:Zero - Starting Life in Another World',
// subtext: 'S3:E6 - Conditions of the Life We Live In',
// image: 'rezero.jpg',
// summary: `Hee-do takes one giant leap closer to her dream. She's eager to tell Yi-jin the good news — but realizes he's disappeared without a trace.`,
// infoSubtext: 'S1:E5 - Episode 5',
// imdbRating: '8.7',
// runtime: '1h 16m',
// date: '2022',
// MPAA: 'TV-14',
// tag: '',
// },
// {
// title: 'Serial Experiments Lain',
// subtext: '1998',
// image: 'lain.jpg',
// tag: '8',
// summary: `Hee-do takes one giant leap closer to her dream. She's eager to tell Yi-jin the good news — but realizes he's disappeared without a trace.`,
// infoSubtext: 'S1:E5 - Episode 5',
// imdbRating: '8.7',
// runtime: '1h 16m',
// date: '2022',
// MPAA: 'TV-14',
// },
// {
// title: 'Monster',
// subtext: '2004-2005',
// image: 'monster.jpg',
// summary: `Hee-do takes one giant leap closer to her dream. She's eager to tell Yi-jin the good news — but realizes he's disappeared without a trace.`,
// infoSubtext: 'S1:E5 - Episode 5',
// imdbRating: '8.7',
// runtime: '1h 16m',
// date: '2022',
// MPAA: 'TV-14',
// tag: '',
// },
// {
// title: 'Bloom Into You',
// subtext: '2018',
// image: 'biy.jpg',
// tag: '13',
// summary: `Hee-do takes one giant leap closer to her dream. She's eager to tell Yi-jin the good news — but realizes he's disappeared without a trace.`,
// infoSubtext: 'S1:E5 - Episode 5',
// imdbRating: '8.7',
// runtime: '1h 16m',
// date: '2022',
// MPAA: 'TV-14',
// },
// {
// title: 'Attack on Titan',
// subtext: '2013-2022',
// image: 'aot.jpg',
// tag: '99+',
// summary: `Hee-do takes one giant leap closer to her dream. She's eager to tell Yi-jin the good news — but realizes he's disappeared without a trace.`,
// infoSubtext: 'S1:E5 - Episode 5',
// imdbRating: '8.7',
// runtime: '1h 16m',
// date: '2022',
// MPAA: 'TV-14',
// },
// {
// title: "Frieren: Beyond Journey's End",
// subtext: '2023-2024',
// image: 'frieren.jpg',
// summary: `Hee-do takes one giant leap closer to her dream. She's eager to tell Yi-jin the good news — but realizes he's disappeared without a trace.`,
// infoSubtext: 'S1:E5 - Episode 5',
// imdbRating: '8.7',
// runtime: '1h 16m',
// date: '2022',
// MPAA: 'TV-14',
// tag: '',
// },
// {
// title: 'Lycoris Recoil',
// subtext: '2022',
// image: 'lycoris.jpg',
// summary: `Hee-do takes one giant leap closer to her dream. She's eager to tell Yi-jin the good news — but realizes he's disappeared without a trace.`,
// infoSubtext: 'S1:E5 - Episode 5',
// imdbRating: '8.7',
// runtime: '1h 16m',
// date: '2022',
// MPAA: 'TV-14',
// tag: '',
// },
// {
// title: 'Tengoku Daimakyo',
// subtext: '2023 - Present',
// image: 'tengoku.jpg',
// summary: `Hee-do takes one giant leap closer to her dream. She's eager to tell Yi-jin the good news — but realizes he's disappeared without a trace.`,
// infoSubtext: 'S1:E5 - Episode 5',
// imdbRating: '8.7',
// runtime: '1h 16m',
// date: '2022',
// MPAA: 'TV-14',
// tag: '',
// },
// ]
export type { FeedItem }
export { ContinueWatching, LatestMovies, LatestAnime }
// export { ContinueWatching, LatestMovies, LatestAnime }

@ -1,6 +1,3 @@
import { Jellyfin } from '@jellyfin/sdk'
import { getLibraryApi } from '@jellyfin/sdk/lib/utils/api/library-api'
function detectBrowser() {
const userAgent = navigator.userAgent
if (userAgent.indexOf('Edg') > -1) {
@ -20,162 +17,179 @@ function detectBrowser() {
return 'Unknown'
}
const jellyfin = new Jellyfin({
clientInfo: {
name: 'Featherfin',
version: '0.0.1',
},
deviceInfo: {
name: detectBrowser(),
id: 'abc123',
},
})
const JF = jellyfin.createApi('http://192.168.0.63:30013')
type Endpoint = {
resource: string
method: string
}
export const Libraries = async () => {
const libs = await getLibraryApi(JF).getMediaFolders()
console.log(libs.data)
return libs.data
type View = {
Id: string
Name: string
CollectionType: string
ParentId: string
}
export default JF
type ViewList = {
Items: View[]
}
// import { useDataStore } from '@/stores/data'
type NextUp = {
Items: Item[]
}
// type Endpoint = {
// resource: string
// method: string
// }
type Item = {
Name: string
OriginalTitle: string
Id: string
Type: string
OfficialRating: string
ImageTags: ImageTags
CommunityRating: number
ProductionYear: number
IndexNumber?: number
ParentIndexNumber?: number
SeriesName?: string
SeriesId?: string
Overview: string
RunTimeTicks: number
ParentThumbItemId?: string
ParentThumbImageTag?: string
ParentBackdropItemId?: string
ParentBackdropImageTags?: string[]
SeriesPrimaryImageTag?: string
UserData?: UserData
}
// type View = {
// Id: string
// Name: string
// CollectionType: string
// ParentId: string
// }
type UserData = {
UnplayedItemCount: number
}
// type NextUp = {
// Items: [Item]
// }
type ImageTags = {
Primary?: string
Thumb?: string
}
// type Item = {
// Name: string
// Id: string
// Type: string
// OfficialRating?: string
// ImageTags: ImageTags
// CommunityRating?: number
// ProductionYear?: number
// IndexNumber?: number
// ParentIndexNumber?: number
// SeriesName?: string
// Overview?: string
// RunTimeTicks: number
// }
type User = {
Name: string
Host: string
Id: string
Views?: View[]
}
// type ImageTags = {
// Primary?: string
// Thumb?: string
// }
export type { User, View, Item, ViewList }
// class JFApi {
// request<T>(host: string, endpoint: Endpoint) {
// return fetch(`${host}${endpoint.resource}`, {
// method: endpoint?.method,
// })
// .then(async (response) => {
// const data = await response.json()
// return data as T
// })
// .catch((error) => {
// return error
// })
// }
// LoadViews(host: string, userid: string) {
// const { setViews } = useDataStore()
// return this.request(host, {
// resource: `/UserViews?userId=${userid}`,
// method: 'GET',
// })
// .then((data) => {
// setViews(data)
// return data
// })
// .catch((err) => {
// return err
// })
// }
// GetLatest(host: string, view: View): Promise<[Item]> {
// return this.request(host, {
// resource: `/Latest&ParentId=${view.Id}`,
// method: 'GET',
// })
// .then((data) => {
// return data
// })
// .catch((err) => {
// return err
// })
// }
// GetNextUp(host: string): Promise<NextUp> {
// return this.request(host, {
// resource: `/Shows/NextUp?Limit=24&Fields=PrimaryImageAspectRatio%2CDateCreated%2CPath%2CMediaSourceCount&UserId=f7967316db8e46b4960690d0df2c0163&ImageTypeLimit=1&EnableImageTypes=Primary%2CBackdrop%2CBanner%2CThumb&EnableTotalRecordCount=false&DisableFirstEpisode=false&NextUpDateCutoff=2023-12-03T21%3A55%3A33.690Z&EnableResumable=false&EnableRewatching=false`,
// method: 'GET',
// })
// .then((data) => {
// return data
// })
// .catch((err) => {
// return err
// })
// }
// GetItem(host: string, userid: string, itemId: string): Promise<Item> {
// return this.request(host, {
// resource: `/User/${userid}/Items/${itemId}`,
// method: 'GET',
// })
// .then((data) => {
// return data
// })
// .catch((err) => {
// return err
// })
// }
// ThumbImageUrl(host: string, itemId: string, imgTag: string) {
// return `${host}/Items/${itemId}/Images/Thumb?fillHeight=267&fillWidth=474&quality=96&tag=${imgTag}`
// }
// PrimaryImageUrl(host: string, itemId: string, imgTag: string) {
// return `${host}/Items/${itemId}/Images/Primary?fillHeight=446&fillWidth=297&quality=96&tag=${imgTag}`
// }
// Login(host: string, username: string, password: string) {
// return fetch(`${host}/Users/authenticatebyname`, {
// method: 'POST',
// body: JSON.stringify({ Username: username, Pw: password }),
// headers: {
// 'Content-Type': 'application/json',
// Authorization:
// 'MediaBrowser Client="Jellyfin Web", Device="Chrome", DeviceId="TW96aWxsYS81LjAgKFdpbmRvd3MgTlQgMTAuMDsgV2luNjQ7IHg2NCkgQXBwbGVXZWJLaXQvNTM3LjM2IChLSFRNTCwgbGlrZSBHZWNrbykgQ2hyb21lLzEyNC4wLjAuMCBTYWZhcmkvNTM3LjM2fDE3MTQ1MzI1OTIxODU1", Version="10.10.3"',
// },
// })
// .then((data) => data.json())
// .then((data) => console.log(data))
// }
// }
class JFApi {
request<T>(endpoint: Endpoint): Promise<T | null> {
const host = localStorage.getItem('jf_host') === null ? '' : localStorage.getItem('jf_host')
return new Promise((resolve) => {
fetch(`${host}${endpoint.resource}`, {
method: endpoint?.method,
headers: {
Authorization: `MediaBrowser Client="Featherfin", Device="${detectBrowser()}", DeviceId="abc123", Version="v0.0.1", Token="${localStorage.getItem('jfauth_token')}"`,
},
})
.then(async (response) => {
const data = await response.json()
resolve(data as T)
})
.catch((error) => {
console.log(error)
resolve(null)
})
})
}
LoadViews(userid: string): Promise<ViewList | null> {
return this.request({
resource: `/UserViews?userId=${userid}`,
method: 'GET',
})
}
GetLatest(userid: string, view: View): Promise<Item[] | null> {
return this.request({
resource: `/Users/${userid}/Items/Latest?Limit=16&Fields=PrimaryImageAspectRatio%2CPath&ImageTypeLimit=1&EnableImageTypes=Primary%2CBackdrop%2CThumb&ParentId=${view.Id}`,
method: 'GET',
})
}
GetNextUp(): Promise<NextUp | null> {
return this.request({
resource: `/Shows/NextUp?Limit=24&Fields=PrimaryImageAspectRatio%2CDateCreated%2CPath%2CMediaSourceCount&UserId=f7967316db8e46b4960690d0df2c0163&ImageTypeLimit=1&EnableImageTypes=Primary%2CBackdrop%2CBanner%2CThumb&EnableTotalRecordCount=false&DisableFirstEpisode=false&NextUpDateCutoff=2023-12-03T21%3A55%3A33.690Z&EnableResumable=false&EnableRewatching=false`,
method: 'GET',
})
}
GetItem(userid: string, itemId: string): Promise<Item | null> {
return this.request({
resource: `/Items/${itemId}?userId=${userid}`,
method: 'GET',
})
}
ThumbImageUrl(itemId: string, imgTag: string) {
const host = localStorage.getItem('jf_host') === null ? '' : localStorage.getItem('jf_host')
return `${host}/Items/${itemId}/Images/Thumb?fillHeight=267&fillWidth=474&quality=96&tag=${imgTag}`
}
BackdropImageUrl(itemId: string, imgTag: string) {
const host = localStorage.getItem('jf_host') === null ? '' : localStorage.getItem('jf_host')
return `${host}/Items/${itemId}/Images/Backdrop?fillHeight=267&fillWidth=474&quality=96&tag=${imgTag}`
}
PrimaryImageUrl(itemId: string, imgTag: string) {
const host = localStorage.getItem('jf_host') === null ? '' : localStorage.getItem('jf_host')
return `${host}/Items/${itemId}/Images/Primary?fillHeight=446&fillWidth=297&quality=96&tag=${imgTag}`
}
Login(host: string, username: string, password: string): Promise<User | null> {
return new Promise((resolve) => {
fetch(`${host}/Users/authenticatebyname`, {
method: 'POST',
body: JSON.stringify({ Username: username, Pw: password }),
headers: {
'Content-Type': 'application/json',
Authorization: `MediaBrowser Client="Featherfin", Device="${detectBrowser()}", DeviceId="abc123", Version="v0.0.1", Token="${localStorage.getItem('jfauth_token')}"`,
},
})
.then((r) => r.json().then((data) => ({ status: r.status, body: data })))
.then((data) => {
console.log(data)
if (data.status !== 200) {
// bad
resolve(null)
} else {
localStorage.setItem('jfauth_token', data.body.AccessToken)
resolve({
Name: data.body.User.Name,
Id: data.body.User.Id,
Host: 'http://192.168.0.63:30013',
})
}
})
})
}
Logout() {
return this.request({
resource: '/Sessions/Logout',
method: 'POST',
})
.then((data) => {
localStorage.removeItem('jfauth_token')
return data
})
.catch((err) => {
return err
})
}
}
// export default new JFApi()
export default new JFApi()
// /*
// - /UserViews?userId=XYZ - returns [Library]
// - /Latest?...&ParentId=Library.Id - returns latest in library
// - /Resume?...&MediaTypes=Video - returns unfinished playback
// - /NextUp?... - returns [Episode]
// - /Users/authenticatebyname body:{Username: "xyz", Pw: "xyz123"} returns: AccessToken, User, SessionInfo
// Authentication is done by appending 'Token="MyTokenXYZ123"' to Authorization header
// - /Users/User.Id - returns: User
// - /Users/User.Id/Items?SortBy=DateCreated%2CSortName%2CProductionYear&SortOrder=Descending&IncludeItemTypes=Movie&Recursive=true&Fields=PrimaryImageAspectRatio%2CMediaSourceCount&ImageTypeLimit=1&EnableImageTypes=Primary%2CBackdrop%2CBanner%2CThumb&StartIndex=0&ParentId=f137a2dd21bbc1b99aa5c0f6bf02a805&Limit=100 - returns [Item<Movie>]
// - /Users/User.Id/Items/Item.Id - FullItem
// - /Items/Item.Id/Similar?userId=User.Id&limit=12&fields=PrimaryImageAspectRatio - returns similar items
// - /Items/Item.Id/Images/Primary?fillHeight=713&fillWidth=500&quality=96&tag=Item.ImageTags.Primary - returns Primary Image
// - /.../Images/Thumb?...&tag=Episode.ParentThumbImageTag
// */
/*
- /UserViews?userId=XYZ - returns [Library]
- /Latest?...&ParentId=Library.Id - returns latest in library
- /Resume?...&MediaTypes=Video - returns unfinished playback
- /NextUp?... - returns [Episode]
- /Users/authenticatebyname body:{Username: "xyz", Pw: "xyz123"} returns: AccessToken, User, SessionInfo
Authentication is done by appending 'Token="MyTokenXYZ123"' to Authorization header
- /Users/User.Id - returns: User
- /Users/User.Id/Items?SortBy=DateCreated%2CSortName%2CProductionYear&SortOrder=Descending&IncludeItemTypes=Movie&Recursive=true&Fields=PrimaryImageAspectRatio%2CMediaSourceCount&ImageTypeLimit=1&EnableImageTypes=Primary%2CBackdrop%2CBanner%2CThumb&StartIndex=0&ParentId=f137a2dd21bbc1b99aa5c0f6bf02a805&Limit=100 - returns [Item<Movie>]
- /Users/User.Id/Items/Item.Id - FullItem
- /Items/Item.Id/Similar?userId=User.Id&limit=12&fields=PrimaryImageAspectRatio - returns similar items
- /Items/Item.Id/Images/Primary?fillHeight=713&fillWidth=500&quality=96&tag=Item.ImageTags.Primary - returns Primary Image
- /.../Images/Thumb?...&tag=Episode.ParentThumbImageTag
- /Sessions/Logout - logout
*/

@ -14,7 +14,7 @@ const router = createRouter({
name: 'login',
component: () => import(/* webpackChunkName: 'login' */ '@/views/LoginView.vue'),
meta: {
hideNavbar: true,
isLoginPage: true,
},
},
],

@ -1,29 +1,22 @@
import { ref } from 'vue'
import { defineStore } from 'pinia'
type UserData = {
host?: string
id?: string
name?: string
views?: [View]
}
type View = {
name: string
mediaType: string
id: string
}
import type { User, View } from '@/jfapi'
export const useDataStore = defineStore('data', () => {
const data = ref<UserData>({})
function setServerHost(host: string) {
data.value.host = host
}
function setViews(views: [View]) {
data.value.views = views
}
function setUser(id: string, name: string) {
data.value.id = id
data.value.name = name
const data = ref<User>({
Name: '',
Host: '',
Id: '',
})
// function setServerHost(host: string) {
// data.value.Host = host
// }
function setViews(views: View[]) {
data.value.Views = views
}
return { data, setServerHost, setViews, setUser }
// function setUser(id: string, name: string) {
// data.value.Id = id
// data.value.Name = name
// }
return { setViews }
})

@ -1,11 +1,29 @@
<script setup lang="ts">
import MediaShowcase from '@/components/MediaShowcase.vue'
import MediaScroller from '@/components/MediaScroller.vue'
import { LatestAnime, LatestMovies } from '@/feed'
import ContinueWatching from '@/components/ContinueWatching.vue'
import { Libraries } from '@/jfapi'
import ContinueWatching from '@/components/NextUp.vue'
import LatestMedia from '@/components/LatestMedia.vue'
import type { View, ViewList } from '@/jfapi'
import { onBeforeMount, ref } from 'vue'
import jfapi from '@/jfapi'
console.log('Libraries =>', Libraries())
const views = ref<View[]>()
onBeforeMount(async () => {
const viewlist = ref<ViewList | null>()
const userid = localStorage.getItem('jf_userid')
const host = localStorage.getItem('jf_host')
console.log('Host =>', host)
viewlist.value = await jfapi.LoadViews(userid ? userid : '')
if (window.location.pathname.toLowerCase() !== '/login') {
if (host === null) {
window.location.href = '/login'
} else if (viewlist.value === null) {
window.location.href = '/login'
}
}
localStorage.setItem('jf_views', JSON.stringify(viewlist.value ? viewlist.value.Items : ''))
views.value = viewlist.value ? viewlist.value.Items : undefined
console.log(views.value)
})
</script>
<template>
@ -13,14 +31,14 @@ console.log('Libraries =>', Libraries())
<MediaShowcase />
<div class="home-nav">
<div class="home-nav-item active">Home</div>
<div class="home-nav-item">4k Movies</div>
<div class="home-nav-item">Anime</div>
<div class="home-nav-item">Movies</div>
<div class="home-nav-item">Shows</div>
<div class="home-nav-item" v-for="view in views" :key="view.Id">{{ view.Name }}</div>
</div>
<div class="media">
<ContinueWatching />
<MediaScroller title="Latest Movies" :items="LatestMovies" />
<MediaScroller title="Latest Anime" :items="LatestAnime" />
<!-- <MediaScroller title="Latest Movies" :items="LatestMovies" />
<MediaScroller title="Latest Anime" :items="LatestAnime" /> -->
<LatestMedia v-for="view of views" :view="view" :key="view.Id" />
</div>
</main>
</template>
@ -28,7 +46,6 @@ console.log('Libraries =>', Libraries())
.home-nav {
display: flex;
justify-content: center;
margin-right: 11.5%;
margin-top: 1.5em;
gap: 3em;
align-items: center;
@ -47,4 +64,7 @@ console.log('Libraries =>', Libraries())
.home-nav-item.active:hover {
background-color: var(--color-text-faded);
}
.media {
margin: 0 10%;
}
</style>

@ -1,18 +1,28 @@
<script setup lang="ts">
import JF from '@/jfapi'
import jfapi from '@/jfapi'
import { ref } from 'vue'
const username = ref('')
const host = ref('http://192.168.0.63:30013')
const password = ref('')
const login = () => {
JF.authenticateUserByName(username.value, password.value).then(() => {
jfapi.Login(host.value, username.value, password.value).then((data) => {
console.log(data)
if (data !== null) {
localStorage.setItem('jf_userid', data.Id)
localStorage.setItem('jf_host', data.Host)
window.location.href = '/'
}
})
}
</script>
<template>
<div class="login-wrapper">
<div class="form-item">
<div class="label">Server Address</div>
<input type="text" v-model="host" />
</div>
<div class="form-item">
<div class="label">Username</div>
<input type="text" v-model="username" />

Loading…
Cancel
Save