Skip to content

Commit 118b1e3

Browse files
vassbojzongkeryurividalgladsonsam
authored
1.5.3 (#2445)
* APlay Updates (#2432) * Fixed custom data location sometimes incorrect - Fixed some songs logged twice in a row - Updated languages * Fixed not marking as played with project section actions * Added fallback API Bible * Edit timer item * Using new scripture URL * Fixed transpose issues - Updated Dutch language * Fix output lyric overrides (#2401) * Fix output lyric overrides by deriving the show template directly from $showsCache * make sure templates set via 'custom template on first slide' don't inherit the overwrites * Cleanup * auto-size caching and optimizing (#2403) * Fetch and populate pingback url * Split AmazingLifeProvider into multiple files. Added CHECK_MEDIA_LICENSE IPC. * Moved encryption decision to the ContentProvider * Made content file a child of Media * Enabled search for content libraries * Brought in click 3 times for aPlay code --------- Co-authored-by: Kristoffer <kristoffervassbo@gmail.com> Co-authored-by: Yuri Vidal <yurividal@users.noreply.github.com> * Added search autoscroll and fixed CSS (#2435) * Added search autoscroll and fixed CSS * Improve Scripture Bible and Collection View * More UI Changes * Fixed imports * Auto error reporting * Create style/stage from output selector popup - New default overlays/templates will be automatically added - Moved "Reset defaults" to context menu - Start playlist action won't restart if already playing - Fixed projects in closed folders not showing in RemoteShow - Fixed projects list not showing in RemoteShow initially - Small fixes * Default project name uses local date format * Scripture template uses style if only one output * Fixed restoring settings data * Delete old backups from within the app * Protocol serving for protected files (#2441) * Rebase to new branch * Use non deprecated protocol * Active template icon in slide editor - Consider donating message - Fixed usage not working by default - Project import does not overwrite show if locked * Scripture selection fixes - Updated outputs code * Fixed YT error - Small tweaks * Fixed stage display video mirror not updating - Tweaked optimized mode - Version update --------- Co-authored-by: Jeremy Zongker <jeremy@zongker.net> Co-authored-by: Yuri Vidal <yurividal@users.noreply.github.com> Co-authored-by: Gladson Sam <115465353+gladsonsam@users.noreply.github.com>
1 parent 4bf4465 commit 118b1e3

File tree

116 files changed

+4032
-1766
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

116 files changed

+4032
-1766
lines changed

package-lock.json

Lines changed: 971 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "freeshow",
3-
"version": "1.5.3-beta.3",
3+
"version": "1.5.3",
44
"private": true,
55
"main": "build/electron/index.js",
66
"description": "Show song lyrics and more for free!",
@@ -105,6 +105,7 @@
105105
"dependencies": {
106106
"@discordjs/opus": "^0.10.0",
107107
"@googleapis/drive": "^8.16.0",
108+
"@sentry/electron": "^7.3.0",
108109
"@tanstack/pacer": "^0.14.0",
109110
"@vimeo/player": "^2.29.3",
110111
"adm-zip": "^0.5.16",

public/lang/en.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -434,7 +434,7 @@
434434
"variable": "Variable",
435435
"trigger": "Trigger",
436436
"audio_stream": "Audio stream",
437-
"now_playing": "NowPlaying file",
437+
"now_playing": "Now playing file",
438438
"aspect_ratio": "Change aspect ratio",
439439
"max_lines": "Set max lines",
440440
"transition": "Transition",

src/electron/IPC/responsesMain.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import * as Sentry from "@sentry/electron/main"
12
import type { BrowserWindow, DesktopCapturerSource } from "electron"
23
import { app, desktopCapturer, screen, shell, systemPreferences } from "electron"
34
import { machineIdSync } from "node-machine-id"
@@ -9,7 +10,7 @@ import { Main } from "../../types/IPC/Main"
910
import type { ErrorLog, LyricSearchResult, OS } from "../../types/Main"
1011
import { openNowPlaying, setPlayingState, unsetPlayingAudio } from "../audio/nowPlaying"
1112
import { ContentProviderRegistry } from "../contentProviders"
12-
import { getBackups, restoreFiles } from "../data/backup"
13+
import { deleteBackup, getBackups, restoreFiles } from "../data/backup"
1314
import { checkIfMediaDownloaded, downloadLessonsMedia, downloadMedia } from "../data/downloadMedia"
1415
import { importShow } from "../data/import"
1516
import { save } from "../data/save"
@@ -85,6 +86,7 @@ export const mainResponses: MainResponses = {
8586
/// //////////////////////
8687
[Main.SAVE]: (a) => save(a),
8788
[Main.BACKUPS]: () => getBackups(),
89+
[Main.DELETE_BACKUP]: (data) => deleteBackup(data),
8890
[Main.IMPORT]: (data) => startImport(data),
8991
[Main.BIBLE]: (data) => loadScripture(data),
9092
[Main.SHOW]: (data) => loadShow(data),
@@ -211,6 +213,14 @@ export const mainResponses: MainResponses = {
211213
return []
212214
}
213215
return await provider.getContent(data.key)
216+
},
217+
[Main.CHECK_MEDIA_LICENSE]: async (data) => {
218+
const provider = ContentProviderRegistry.getProvider(data.providerId)
219+
if (!provider?.checkMediaLicense) {
220+
console.error(`Provider ${data.providerId} does not support checkMediaLicense`)
221+
return null
222+
}
223+
return await provider.checkMediaLicense(data.mediaId)
214224
}
215225
}
216226

@@ -410,6 +420,20 @@ export function createLog(err: Error) {
410420
} as ErrorLog
411421
}
412422

423+
export function autoErrorReport() {
424+
if (!isProd) return
425+
426+
Sentry.init({
427+
dsn: "https://5d1069c3cb6faaa6e7ad0d9dc0145361@o4510419080445952.ingest.us.sentry.io/4510419082346496",
428+
beforeSend(event) {
429+
// filter out known non-critical errors
430+
const errorMessage = event.exception?.values?.[0]?.value || ""
431+
const shouldFilter = ERROR_FILTER.some((filter) => errorMessage.includes(filter))
432+
return shouldFilter ? null : event
433+
},
434+
})
435+
}
436+
413437
// STORE MEDIA AS BASE64
414438
// function storeMedia(files: { id: string; path: string }[]) {
415439
// let encodedFiles: { id: string; content: string }[] = []

src/electron/audio/nowPlaying.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ async function convertDynamicValues(data: NowPlayingData) {
8181
const coverFilePath = join(audioFolder, fileNameImage)
8282
if (value === "{artwork_path}") return coverFilePath
8383

84+
if (!doesPathExist(coverFilePath)) return ""
8485
const pngBuffer = readFile(coverFilePath)
8586
const base64String = Buffer.from(pngBuffer).toString('base64')
8687
return pngBuffer ? `data:image/png;base64,${base64String}` : ""
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import express from "express"
2+
import { ToMain } from "../../../types/IPC/ToMain"
3+
import { getContentProviderAccess, setContentProviderAccess } from "../../data/contentProviders"
4+
import { sendToMain } from "../../IPC/main"
5+
import { getKey } from "../../utils/keys"
6+
import { OAuth2Helper } from "../base/OAuth2Helper"
7+
import type { AmazingLifeAuthData, AmazingLifeScopes } from "./types"
8+
import { AMAZING_LIFE_OAUTH_BASE } from "./types"
9+
10+
/**
11+
* Handles authentication and connection management with the AmazingLife (APlay) service.
12+
* Manages OAuth PKCE flow, token refresh, and connection state.
13+
*
14+
* WARNING: This class should ONLY be accessed through AmazingLifeProvider.
15+
* Do not import or use this class directly in other parts of the application.
16+
* Use ContentProviderRegistry or AmazingLifeProvider instead.
17+
*/
18+
export class AmazingLifeConnect {
19+
private static readonly AMAZING_LIFE_PORT = 5502
20+
private static AMAZING_LIFE_ACCESS: AmazingLifeAuthData | null = null
21+
private static readonly clientId: string = getKey("amazinglife_id") || ""
22+
private static oauthHelper: OAuth2Helper<AmazingLifeAuthData>
23+
private static app = express()
24+
private static routeSetup = false
25+
26+
private static initializeOAuthHelper(): void {
27+
if (!this.oauthHelper) {
28+
const redirectUri = `http://localhost:${this.AMAZING_LIFE_PORT}/auth/complete`
29+
this.oauthHelper = new OAuth2Helper<AmazingLifeAuthData>({
30+
clientId: this.clientId,
31+
clientSecret: "", // Not needed for PKCE flow
32+
authUrl: `${AMAZING_LIFE_OAUTH_BASE}/authorize`,
33+
tokenUrl: `${AMAZING_LIFE_OAUTH_BASE}/token`,
34+
redirectUri,
35+
scopes: ["openid", "profile", "email"],
36+
usePKCE: true,
37+
additionalParams: { state: "xyz" }
38+
})
39+
}
40+
41+
// Set up the auth callback route
42+
if (!this.routeSetup) {
43+
this.app.get('/auth/complete', (req, res) => {
44+
this.handleAuthCallback(req, res)
45+
})
46+
this.routeSetup = true
47+
}
48+
}
49+
50+
public static async connect(scope: AmazingLifeScopes): Promise<AmazingLifeAuthData | null> {
51+
const storedAccess = getContentProviderAccess("amazinglife", scope) as AmazingLifeAuthData | null
52+
53+
if (storedAccess) {
54+
this.AMAZING_LIFE_ACCESS = storedAccess
55+
56+
if (this.isTokenExpired(storedAccess)) {
57+
console.info("APlay: Token expired, refreshing...")
58+
const refreshed = await this.refreshToken(scope)
59+
if (refreshed) {
60+
this.AMAZING_LIFE_ACCESS = refreshed
61+
sendToMain(ToMain.PROVIDER_CONNECT, { providerId: "amazinglife", success: true })
62+
return refreshed
63+
}
64+
} else {
65+
console.info("APlay: Using valid stored credentials")
66+
sendToMain(ToMain.PROVIDER_CONNECT, { providerId: "amazinglife", success: true })
67+
return storedAccess
68+
}
69+
}
70+
71+
console.info("APlay: Starting OAuth flow...")
72+
const authData = await this.authenticate(scope)
73+
sendToMain(ToMain.PROVIDER_CONNECT, {
74+
providerId: "amazinglife",
75+
success: !!authData,
76+
isFirstConnection: !!authData
77+
})
78+
return authData
79+
}
80+
81+
public static disconnect(scope: AmazingLifeScopes = "openid profile email"): void {
82+
console.log(`APlay: Disconnecting (scope: ${scope})`)
83+
setContentProviderAccess("amazinglife", scope, null)
84+
this.AMAZING_LIFE_ACCESS = null
85+
}
86+
87+
public static getAccessToken(): string | null {
88+
return this.AMAZING_LIFE_ACCESS?.access_token || null
89+
}
90+
91+
private static isTokenExpired(access: AmazingLifeAuthData): boolean {
92+
return (access.created_at + access.expires_in) * 1000 < Date.now()
93+
}
94+
95+
private static async refreshToken(scope: AmazingLifeScopes): Promise<AmazingLifeAuthData | null> {
96+
if (!this.AMAZING_LIFE_ACCESS?.refresh_token) {
97+
console.warn("No refresh token available for APlay")
98+
return null
99+
}
100+
101+
try {
102+
this.initializeOAuthHelper()
103+
const refreshed = await this.oauthHelper.refreshAccessToken(this.AMAZING_LIFE_ACCESS.refresh_token, scope)
104+
if (refreshed) {
105+
this.AMAZING_LIFE_ACCESS = refreshed
106+
setContentProviderAccess("amazinglife", scope, refreshed)
107+
}
108+
return refreshed
109+
} catch (error) {
110+
console.error("Failed to refresh APlay token:", error)
111+
return null
112+
}
113+
}
114+
115+
private static async authenticate(scope: AmazingLifeScopes): Promise<AmazingLifeAuthData | null> {
116+
this.initializeOAuthHelper()
117+
118+
const server = this.app.listen(this.AMAZING_LIFE_PORT, () => {
119+
console.info(`Listening for APlay OAuth response at port ${this.AMAZING_LIFE_PORT}`)
120+
})
121+
122+
server.once("error", (err: Error) => {
123+
if ((err as any).code === "EADDRINUSE") server.close()
124+
})
125+
126+
try {
127+
const authData = await this.oauthHelper.authorize(scope)
128+
if (authData) {
129+
this.AMAZING_LIFE_ACCESS = authData
130+
setContentProviderAccess("amazinglife", scope, authData)
131+
}
132+
return authData
133+
} catch (error) {
134+
console.error("APlay authentication failed:", error)
135+
return null
136+
} finally {
137+
server.close()
138+
}
139+
}
140+
141+
public static handleAuthCallback(req: express.Request, res: express.Response): void {
142+
this.initializeOAuthHelper()
143+
this.oauthHelper.handleCallback(req, res)
144+
}
145+
}

0 commit comments

Comments
 (0)