diff --git a/index.html b/index.html index b8fb712..53f4560 100644 --- a/index.html +++ b/index.html @@ -7,10 +7,12 @@ Jellyfin Rewind - +
-
+
+ + -
by ${() => +
by ${() => state.settings.useAlbumArtists ? track.albumBaseInfo.albumArtistBaseInfo.name : track.artistsBaseInfo @@ -287,12 +280,12 @@ state.features = [
-
+
Streamed ${() => showAsNumber(state.rewindReport.artists?.[state.settings.rankingMetric]?.[0]?.playCount[state.settings.dataSource])} times.
Listened to ${() => showAsNumber(state.rewindReport.artists?.[state.settings.rankingMetric]?.[0]?.uniqueTracks)} unique songs
for ${() => showAsNumber(state.rewindReport.artists?.[state.settings.rankingMetric]?.[0]?.totalPlayDuration[state.settings.dataSource].toFixed(0))} minutes.
-
+
`), @@ -383,12 +376,12 @@ state.features = [
-
+
Streamed ${() => showAsNumber(state.rewindReport.albums?.[state.settings.rankingMetric]?.[0]?.playCount[state.settings.dataSource])} times.
Listened for ${() => showAsNumber(state.rewindReport.albums?.[state.settings.rankingMetric]?.[0]?.totalPlayDuration[state.settings.dataSource]?.toFixed(0))} minutes.
-
+
`), @@ -409,11 +402,11 @@ state.features = [ ${() => index + 1}. ${() => album.name}
- by ${() => + by ${() => state.settings.useAlbumArtists ? album.albumArtist.name : album.artists - .reduce((acc, cur, index) => index > 0 ? `${acc} & ${cur.name}` : cur.name, ``) + ?.reduce((acc, cur, index) => index > 0 ? `${acc} & ${cur.name}` : cur.name, ``) || `Unknown Artist` }
@@ -444,11 +437,11 @@ state.features = [ ${() => index + 1 + 5}. ${() => album.name}
-
by ${() => +
by ${() => state.settings.useAlbumArtists ? album.albumArtist.name : album.artists - .reduce((acc, cur, index) => index > 0 ? `${acc} & ${cur.name}` : cur.name, ``) + ?.reduce((acc, cur, index) => index > 0 ? `${acc} & ${cur.name}` : cur.name, ``) || `Unknown Artist` }
- //
- //
- //
} function handleFeatureClick(event) { @@ -1239,8 +1268,11 @@ function handleFeatureClick(event) { console.log(event) let featureElement = event.target.closest(`[data-feature-name]`) console.log(`featureElement:`, featureElement) + console.log(`event.clientX:`, event.clientX) + console.log(`featureElement.offsetLeft:`, featureElement.offsetLeft) + console.log(`featureElement.getBoundingClientRect().x:`, featureElement.getBoundingClientRect().x) - if (event.clientX < featureElement.offsetWidth / 3) { + if ((event.clientX - featureElement.getBoundingClientRect().x) < featureElement.offsetWidth / 3) { previous() } else { next() @@ -1249,8 +1281,6 @@ function handleFeatureClick(event) { function showPlaytimeByMonthChart() { - //TODO disable if playback reporting isn't enabled - console.log(`Loading chart...`) let canvas; @@ -1423,9 +1453,9 @@ function loadMostSuccessivePlaysTrackMedia() { const mostSuccessivePlaysTrackPrimaryImage = document.querySelector(`#most-successive-streams-track-image`); const mostSuccessivePlaysTrackBackgroundImage = document.querySelector(`#most-successive-streams-track-background-image`); console.log(`img:`, mostSuccessivePlaysTrackPrimaryImage) - const mostSuccessivePlaysTrack = state.rewindReport.generalStats.mostSuccessivePlays.track - console.log(`mostSuccessivePlaysTrack:`, mostSuccessivePlaysTrack) - state.jellyHelper.loadImage([mostSuccessivePlaysTrackPrimaryImage, mostSuccessivePlaysTrackBackgroundImage], mostSuccessivePlaysTrack.image, `track`, state.settings.darkMode) + // const mostSuccessivePlaysTrack = state.rewindReport.generalStats.mostSuccessivePlays.track + // console.log(`mostSuccessivePlaysTrack:`, mostSuccessivePlaysTrack) + state.jellyHelper.loadImage([mostSuccessivePlaysTrackPrimaryImage, mostSuccessivePlaysTrackBackgroundImage], state.rewindReport.generalStats.mostSuccessivePlays?.image, `track`, state.settings.darkMode) } @@ -1453,7 +1483,7 @@ function playTopTracks() { async function playTopArtist() { - const topArtistByDuration = state.rewindReport.artists?.[state.settings.rankingMetric]?.[0] //TODO adhere to settings for ranking + const topArtistByDuration = state.rewindReport.artists?.[state.settings.rankingMetric]?.[0] console.log(`topArtistByDuration:`, topArtistByDuration) let artistsTracks = await state.jellyHelper.loadTracksForGroup(topArtistByDuration.id, `artist`) @@ -1485,7 +1515,7 @@ async function playTopArtists() { async function playTopAlbum() { - const topAlbumByDuration = state.rewindReport.albums?.[state.settings.rankingMetric]?.[0] //TODO adhere to settings for ranking + const topAlbumByDuration = state.rewindReport.albums?.[state.settings.rankingMetric]?.[0] console.log(`topAlbumByDuration:`, topAlbumByDuration) let albumsTracks = await state.jellyHelper.loadTracksForGroup(topAlbumByDuration.id, `album`) @@ -1535,9 +1565,11 @@ async function playTopGenres() { function playMostSuccessivePlaysTrack() { - const mostSuccessivePlaysTrack = state.rewindReport.generalStats.mostSuccessivePlays.track - console.log(`topSongByDuration:`, mostSuccessivePlaysTrack) - fadeToNextTrack(mostSuccessivePlaysTrack) + const mostSuccessivePlaysTrack = state.rewindReport.generalStats.mostSuccessivePlays?.track + console.log(`mostSuccessivePlaysTrack:`, mostSuccessivePlaysTrack) + if (mostSuccessivePlaysTrack) { + fadeToNextTrack(mostSuccessivePlaysTrack) + } } @@ -1607,28 +1639,40 @@ async function fadeToNextTrack(trackInfo) { inactivePlayer.volume = 0 activePlayer.volume = 1 inactivePlayer.play() - - // fade - let fadeInterval = setInterval(() => { - try { - if (inactivePlayer.volume + 0.1 <= 1) { - inactivePlayer.volume += 0.1 - activePlayer.volume -= 0.1 - } else { - clearInterval(fadeInterval) + inactivePlayer.volume = 0 + + // I hate Safari + const fadePerStep = 0.05 + const fadeDuration = 1000 + const fadeStepsOut = Array(1 / fadePerStep).fill(1).map((_, i) => Number((1 - i * fadePerStep).toFixed(2))) + const fadeStepsIn = Array(1 / fadePerStep).fill(1).map((_, i) => Number((i * fadePerStep).toFixed(2))) + fadeStepsIn.unshift(0, 0, 0, 0, 0, 0, 0, 0, 0, 0) + + const doFade = (stepIndex) => () => { + try { + inactivePlayer.volume = fadeStepsIn[stepIndex] + activePlayer.volume = fadeStepsOut[stepIndex] || 0 + if (stepIndex === fadeStepsIn.length - 1) { + // stop the active player activePlayer.pause() activePlayer.currentTime = 0 + //!!! don't reset currentTime, otherwise the track will start from the beginning when resuming playback + } else { + setTimeout(doFade(stepIndex + 1), fadeDuration / fadeStepsIn.length) } } catch (err) { console.error(`Error while fading tracks:`, err) - clearInterval(fadeInterval) inactivePlayer.volume = 1 activePlayer.volume = 0 activePlayer.pause() } - }, 100) + } + + // fade + doFade(0)() + } } @@ -1659,30 +1703,35 @@ function pausePlayback() { player1.volume = 1 player2.volume = 1 - // fade - let fadeInterval = setInterval(() => { + const fadePerStep = 0.05 + const fadeDuration = 750 + const fadeSteps = Array(1 / fadePerStep).fill(1).map((_, i) => Number((1 - i * fadePerStep).toFixed(2))) + + const doFade = (stepIndex) => () => { try { - if (player1.volume - 0.1 >= 0) { - player1.volume -= 0.1 - player2.volume -= 0.1 - } else { - clearInterval(fadeInterval) + player1.volume = fadeSteps[stepIndex] + player2.volume = fadeSteps[stepIndex] + if (stepIndex === fadeSteps.length - 1) { // stop the active player player1.pause() player2.pause() //!!! don't reset currentTime, otherwise the track will start from the beginning when resuming playback + } else { + setTimeout(doFade(stepIndex + 1), fadeDuration / fadeSteps.length) } } catch (err) { console.error(`Error while fading tracks:`, err) - clearInterval(fadeInterval) player1.volume = 0 player2.volume = 0 player1.pause() player2.pause() } - }, 100) + } + // fade + doFade(0)() + } // uses the tag data to determine the previously active player and resumes playback by fading it in @@ -1706,28 +1755,33 @@ function resumePlayback() { activePlayer.volume = 0 activePlayer.play() - // fade - let fadeInterval = setInterval(() => { + const fadePerStep = 0.05 + const fadeDuration = 750 + const fadeSteps = Array(1 / fadePerStep).fill(1).map((_, i) => Number((i * fadePerStep).toFixed(2))) + + const doFade = (stepIndex) => () => { try { - if (activePlayer.volume + 0.1 <= 1) { - activePlayer.volume += 0.1 - } else { - clearInterval(fadeInterval) + activePlayer.volume = fadeSteps[stepIndex] + + if (stepIndex !== fadeSteps.length - 1) { + setTimeout(doFade(stepIndex + 1), fadeDuration / fadeSteps.length) } } catch (err) { console.error(`Error while fading tracks:`, err) - clearInterval(fadeInterval) activePlayer.volume = 1 } - }, 100) - + } + + // fade + doFade(0)() + } function showAsNumber(numberOrArray) { if (Array.isArray(numberOrArray)) { numberOrArray = numberOrArray.length } - return numberOrArray.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); + return numberOrArray?.toString()?.replace(/\B(?=(\d{3})+(?!\d))/g, ","); } // function stringToColor(string) { diff --git a/src/jelly-helper.js b/src/jelly-helper.js index 56cbe68..26057da 100644 --- a/src/jelly-helper.js +++ b/src/jelly-helper.js @@ -8,15 +8,13 @@ export default class JellyHelper { loadImage(elements, imageInfo, type = `track`, isDarkMode = false) { - console.log(`isDarkMode:`, isDarkMode) - if (!Array.isArray(elements)) { elements = [elements]; } - const blurhash = imageInfo.blurhash - const primaryTag = imageInfo.primaryTag - const parentItemId = imageInfo.parentItemId + const blurhash = imageInfo?.blurhash + const primaryTag = imageInfo?.primaryTag + const parentItemId = imageInfo?.parentItemId if (blurhash) { const dataUri = blurhashToDataURI(blurhash) @@ -33,9 +31,9 @@ export default class JellyHelper { case `artist`: element.src = `/media/ArtistPlaceholder${isDarkMode ? `-dark` : ``}.png` break; - case `album`: - element.src = `/media/AlbumPlaceholder${isDarkMode ? `-dark` : ``}.png` - break; + case `album`: + element.src = `/media/AlbumPlaceholder${isDarkMode ? `-dark` : ``}.png` + break; default: break; @@ -43,8 +41,12 @@ export default class JellyHelper { }) } - if (primaryTag && parentItemId) { - fetch(`${this.auth.config.baseUrl}/Items/${parentItemId}/Images/Primary?tag=${primaryTag}`, { + if (primaryTag && (parentItemId || type === `user`)) { + let url = `${this.auth.config.baseUrl}/Items/${parentItemId}/Images/Primary?tag=${primaryTag}` + if (type === `user`) { + url = `${this.auth.config.baseUrl}/Users/${parentItemId}/Images/Primary?tag=${primaryTag}` + } + fetch(url, { method: `GET`, headers: { ...this.auth.config.defaultHeaders, @@ -76,9 +78,12 @@ export default class JellyHelper { case `artist`: element.src = `/media/ArtistPlaceholder${isDarkMode ? `-dark` : ``}.png` break; - case `album`: - element.src = `/media/AlbumPlaceholder${isDarkMode ? `-dark` : ``}.png` - break; + case `album`: + element.src = `/media/AlbumPlaceholder${isDarkMode ? `-dark` : ``}.png` + break; + case `user`: + element.src = `/media/ArtistPlaceholder${isDarkMode ? `-dark` : ``}.png` + break; default: break; @@ -100,7 +105,7 @@ export default class JellyHelper { 'UserId': this.auth.config.user.id, 'DeviceId': this.auth.config.user.deviceId, 'api_key': this.auth.config.user.token, - 'Container': `opus,webm|opus,mp3,aac,m4a|aac,m4b|aac,flac,webma,webm|webma,wav,ogg`, + 'Container': `opus,webm|opus,mp3,aac,m4a|aac,m4b|aac,flac,webma,webm|webma,wav,ogg`, // limit to mp3 for best support 'TranscodingContainer': `ts`, 'TranscodingProtocol': `hls`, 'AudioCodec': `aac`, diff --git a/src/onboarding.js b/src/onboarding.js new file mode 100644 index 0000000..95a6455 --- /dev/null +++ b/src/onboarding.js @@ -0,0 +1,575 @@ +import { reactive, watch, html, } from '@arrow-js/core' + +import { connectToServer, generateRewindReport, initializeFeatureStory, loginViaAuthToken, loginViaPassword, restoreAndPrepareRewind } from './setup'; + +export const state = reactive({ + currentView: `start`, + views: {}, + server: { + url: ``, + users: [ + {name: `test`, id: `test`}, + {name: `test`, id: `test`}, + {name: `test`, id: `test`}, + {name: `test`, id: `test`} + ], + loginType: `password`, + selectedUser: null, + }, + rewindGenerating: false, + rewindReport: null, + staleReport: false, + progress: 0, + auth: null, + error: null, + connectionHelpDialogOpen: false, + featuresInitialized: false, + darkMode: null, +}) + +export async function init(auth) { + + state.views = reactive({ + start: viewStart, + server: viewServer, + user: viewUser, + login: viewLogin, + load: viewLoad, + revisit: viewRevisit, + rewindGenerationError: viewRewindGenerationError, + }) + + state.auth = auth + + // MediaQueryList + const darkModePreference = window.matchMedia("(prefers-color-scheme: dark)"); + + // recommended method for newer browsers: specify event-type as first argument + darkModePreference.addEventListener(`change`, e => { + if (e.matches) { + state.darkMode = true + } else { + state.darkMode = false + } + }); + + state.darkMode = darkModePreference.matches + + handleBackButton() + + try { + let restored = await restoreAndPrepareRewind() + + state.rewindReport = restored.rewindReportData + state.staleReport = restored.staleReport + console.log(`state.auth.config.user:`, state.auth.config.user) + state.currentView = `revisit` + } catch (err) { + if (state.auth.config.user) { + state.currentView = `load` + } + } + +} + +function handleBackButton() { + + // add hash to url on view change + watch(() => state.currentView, (view) => { + let url = new URL(location.href) + url.hash = view + history.pushState(null, null, url); + + }) + + // handle back button by changing state.currentView + // history.pushState(null, null, location.href); + window.onpopstate = () => { + // console.log(`back`) + // history.go(1); + state.currentView = document.location.hash.slice(1) + }; +} + +export function render() { + const onboardingElement = document.querySelector(`#onboarding`) + + html` +
+ ${() => state.views[state.currentView]} + ${() => state.connectionHelpDialogOpen ? connectionHelpDialog : null} +
+ `(onboardingElement) +} + +watch(() => [ + console.log(`state.currentView:`, state.currentView) +]) + +const header = html` +
+ Jellyfin Rewind Logo +

Rewind

+
+` + +const viewStart = html` +
+ + ${() => header} + +
+

Hi there!

+

Before we can get started with your rewind, you'll have to log into your Jellyfin server.

+

Ideally, your server is reachable over the internet and via secure HTTPS, but even if not, there are ways to enjoy your Rewind.

+

Let's get started!

+
+ + + +
+` + +const connectionHelpDialog = html` +
+
+
+
+

How to Connect?

+ +
+
+
+

Make sure your using the same protocol (https or http) as your server is using.

+

Make sure you're not using a local IP address or mDNS hostname. For example, you could use your server's Tailscale IP address, if you use Tailscale as your VPN.

+

Because Jellyfin Rewind is web-based and (for now at least) not available as a plugin, it might be a bit tricky to get your browser to communicate with your Jellyfin server. The problem is that browsers won't allow "insecure" requests (http) from a "secure" website (https).

+

Therefore, if you're unsure what your Jellyfin server is using, simply use the first link (http)!

+

Here are the links to the Jellyfin Rewind website:

+

HTTP
(works for both http and https Jellyfin servers, but some things might not work correctly):
http://jellyfin-rewind-http.chaphasilor.xyz

+

HTTPS
(if your Jellyfin server has an https connection, this is the best experience):
https://jellyfin-rewind.chaphasilor.xyz

+ +
+

Advanced users can also download the zip-archive and use a local web server to serve the files, because local IPs/domains are treated differently by browsers.

+
+
+
+
+ +` + +async function connect() { + state.error = null + state.server.url = document.querySelector(`#onboarding-server-url`).value + try { + state.server.users = await connectToServer(state.auth, state.server.url) + state.currentView = `user` + } catch (err) { + console.error(`Error while connecting to the server:`, err) + state.error = html` +
+

There was an error while connecting to the server.

+

Please check the URL and try again.

+ +
+ ` + } +} + +const viewServer = html` +
+ + ${() => header} + +
+

First, type in the web address (URL) of your Jellyfin server.

+

If you don't know the URL, you can open your Jellyfin app, open the menu/sidebar and click on "Select Server". It should display your server's URL and you can easily copy it!

+
+ +
+ +
+ + + + ${() => state.error} + +
+` + +const viewUser = html` +
+ + ${() => header} + +
+

That worked, amazing!

+

Now, select the user account you want to see the Rewind for.

+ +

You can also manually enter a username if your account isn't shown.
Alternatively, connecting via an access token is possible as well!

+
+ +
+ Select a User +
    +
    + ${() => state.server.users.map(user => html` +
  • + + ${user.Name} +
  • + `)} +
    + or +
    +
  • + Manually enter username +
  • + or +
  • + Log in via auth token +
  • +
    +
+
+ +
+` + +async function login() { + const username = document.querySelector(`#onboarding-username`).value + const password = document.querySelector(`#onboarding-password`).value + try { + let userInfo = await loginViaPassword(state.auth, username, password) + state.currentView = `load` + } catch (err) { + console.error(`Error while logging in:`, err) + //TODO error handling + } +} + +async function loginAuthToken() { + const token = document.querySelector(`#onboarding-auth-token`).value + try { + let userInfo = await loginViaAuthToken(state.auth, username, token) + state.currentView = `load` + } catch (err) { + console.error(`Error while logging in:`, err) + //TODO error handling + } +} + +const viewLogin = html` +
+ + ${() => header} + +
+

Almost there!

+

Please enter your credentials to log into your server.
Your password will only be sent to your server.

+
+ +
+ ${() => + [`password`, `full`].includes(state.server.loginType) ? html` + + + ` : html` + + ` + } +
+ + + +
+` + +const progressBar = html` +
+ +
+
+
+
+` + +function smoothSeek(current, target, duration) { + const diff = Math.abs(target - current) + const stepCount = 20 + const step = Math.abs(diff / stepCount) + + const increase = () => { + if (state.progress < target) { + state.progress += step + setTimeout(increase, duration / stepCount) + } + } + increase() +} + +async function generateReport() { + try { + state.rewindGenerating = true + state.progress = 0 + state.rewindReport = await generateRewindReport((progress) => { + smoothSeek(state.progress, progress, 750) + // state.progress = progress + }) + state.rewindGenerating = false + } catch (err) { + console.error(err) + state.rewindGenerating = false + state.currentView = `rewindGenerationError` + } +} + +watch(() => state.currentView, async (view) => { + if (view === `load`) { + await generateReport() + } +}) + +function launchRewind() { + initializeFeatureStory(state.rewindReport, state.featuresInitialized) + state.featuresInitialized = true + state.currentView = `revisit` +} + +const viewLoad = html` +
+ + ${() => header} + +
+

Awesome, you're now logged in!

+

Your Rewind Report is now generating. This might take a few seconds.

+

Please be patient, and if nothing happens for more than 30s, reach out to me via Reddit so that I can look into it :)

+
+ + ${() => progressBar} + + ${() => + state.rewindReport ? html` + + ` : html`
` + } + + + +
+` + +const viewRevisit = html` +
+ + ${() => header} + +
+

Welcome back, ${() => state.auth.config?.user?.name}!

+

${() => + !state.staleReport ? + html` + Your Rewind Report is still saved. You can view it any time you like. + ` : html` + The stored Rewind report is stale. Please re-generate it for the best experience. + ` + }

+
+ + + + + + + +
+` + +const viewRewindGenerationError = html` +
+ + ${() => header} + +
+

Sorry, we couldn't generate your Rewind Report.

+

Please try again later.

+

If you keep seeing this message, please reach out to me on Reddit or Twitter so that I can try to resolve the issue!

+
+ + + +
+` diff --git a/src/rewind.js b/src/rewind.js index ef5b0a6..18010b5 100644 --- a/src/rewind.js +++ b/src/rewind.js @@ -20,8 +20,6 @@ const playbackReportQuery = (year) => { // GROUP BY ItemId -- don't group so that we can filter out wrong durations // LIMIT 200 -//TODO implement batched requests to not exceed maximum URL length - async function loadPlaybackReport(year) { const response = await fetch(`${auth.config.baseUrl}/user_usage_stats/submit_custom_query?stamp=${Date.now()}`, { @@ -115,8 +113,9 @@ function indexPlaybackReport(playbackReportJSON) { const items = {} for (const item of playbackReportJSON.items) { + const isoDate = item.DateCreated.replace(` `, `T`) + `Z` // Safari doesn't seem to support parsing the raw dates from playback reporting (RFC 3339) const playInfo = { - date: new Date(item.DateCreated), + date: new Date(isoDate), duration: Number(item.PlayDuration), client: item.ClientName, device: item.DeviceName, @@ -298,9 +297,10 @@ function chunkedArray(array, chunkSize) { return chunks } -async function generateRewindReport(year) { +async function generateRewindReport(year, progressCallback = () => {}) { console.info(`Generating Rewind Report for ${year}...`) + progressCallback(0) let playbackReportAvailable = true let playbackReportComplete = true @@ -314,12 +314,14 @@ async function generateRewindReport(year) { playbackReportInfo = null playbackReportAvailable = false } + progressCallback(0.2) const playbackReportJSON = generateJSONFromPlaybackReport(playbackReportInfo) console.log(`playbackReportJSON:`, playbackReportJSON) if (playbackReportJSON.items.length === 0) { playbackReportDataMissing = true } + progressCallback(0.25) // const allItemInfo = [] @@ -330,25 +332,33 @@ async function generateRewindReport(year) { // } const allItemInfo = (await loadItemInfo()).Items; + progressCallback(0.3) console.log(`allItemInfo:`, allItemInfo) const allItemInfoIndexed = indexItemInfo(allItemInfo) + progressCallback(0.4) const enhancedPlaybackReportJSON = adjustPlaybackReportJSON(playbackReportJSON, allItemInfoIndexed) + progressCallback(0.5) const indexedPlaybackReport = indexPlaybackReport(enhancedPlaybackReportJSON) console.log(`indexedPlaybackReport:`, indexedPlaybackReport) + progressCallback(0.6) console.log(`Object.keys(allItemInfoIndexed).length:`, Object.keys(allItemInfoIndexed).length) const allTopTrackInfo = aggregate.generateTopTrackInfo(allItemInfoIndexed, indexedPlaybackReport) + progressCallback(0.7) const artistInfo = indexArtists(await loadArtistInfo()) console.log(`artistInfo:`, artistInfo) + progressCallback(0.75) const albumInfo = indexAlbums(await loadAlbumInfo()) console.log(`albumInfo:`, albumInfo) + progressCallback(0.8) const totalStats = aggregate.generateTotalStats(allTopTrackInfo, enhancedPlaybackReportJSON) + progressCallback(0.95) const jellyfinRewindReport = { commit: __COMMITHASH__, @@ -456,6 +466,8 @@ async function generateRewindReport(year) { } console.log(`jellyfinRewindReport:`, jellyfinRewindReport) + + progressCallback(1) rewindReport = jellyfinRewindReport diff --git a/src/setup.js b/src/setup.js new file mode 100644 index 0000000..8280a96 --- /dev/null +++ b/src/setup.js @@ -0,0 +1,99 @@ +import * as Features from './features.js' + +export async function connectToServer(auth, serverUrl) { + let userInfo + try { + await auth.connectToServer(serverUrl) + userInfo = await auth.fetchUsers() + console.info(`Connected to server!`) + console.info(`Users:`, userInfo) + } catch (err) { + console.error(`Error while connecting to the server:`, err) + throw new Error(`Error while connecting to the server. Make sure your using the same protocol (https or http) as your server is using. Make sure you're not using a local IP address or mDNS hostname. For example, you could use your server's Tailscale IP address, if you use Tailscale as your VPN.`) + } + return userInfo +} + +export async function loginViaPassword(auth, username, password) { + + await auth.authenticateUser(username, password) + console.info(`Successfully logged in as ${username}`) + auth.saveSession() + + return auth.config.user + +} + +export async function loginViaAuthToken(auth, authToken) { + + await auth.authenticateUserViaToken(authToken) + console.info(`Successfully logged in as ${auth.config.name}`) + auth.saveSession() + + return auth.config.user + +} + +export function initializeFeatureStory(report, featuresInitialized) { + + Features.openFeatures() + + Features.init(report, window.helper, window.jellyfinRewind.auth) + if (!featuresInitialized) { + Features.render() + } + +} +window.initializeFeatureStory = initializeFeatureStory + +export async function restoreAndPrepareRewind() { + + let rewindReportData = null + let staleReport = false + try { + rewindReportData = { + jellyfinRewindReport: jellyfinRewind.restoreRewindReport() + } + + if (rewindReportData.jellyfinRewindReport.commit !== __COMMITHASH__) { + staleReport = true + } + // check if the report is for the previous year and it's after February + if (rewindReportData.jellyfinRewindReport.year !== new Date().getFullYear() && new Date().getMonth() > 1) { + staleReport = true + } + + } catch (err) { + console.warn(`Couldn't restore Rewind report:`, err) + throw new Error(`Couldn't restore Rewind report.`) + } + + return { + rewindReportData, + staleReport, + } +} + +export async function generateRewindReport(progressCallback) { + + let reportData + try { + + reportData = await window.jellyfinRewind.generateRewindReport(Number(import.meta.env.VITE_TARGET_YEAR), progressCallback) + console.info(`Report generated successfully!`) + + } catch (err) { + throw new Error(`Error while generating the report:`, err) + } + + try { + window.jellyfinRewind.saveRewindReport() + console.info(`Report saved successfully!`) + } catch (err) { + console.error(`Couldn't save Rewind report:`, err) + } + + return reportData + +} +window.generateRewindReport = generateRewindReport