From 621d1644027e9c3ef8aeca69ec8699d0ad35efe4 Mon Sep 17 00:00:00 2001 From: Lance Chant <13349722+lancechant@users.noreply.github.com> Date: Sat, 20 Sep 2025 12:35:00 +0200 Subject: [PATCH 01/29] feat: added more subtitle customization options Subtitles can now be customized with the following extra options: - Colour - background opacity/colour - outline opacity/colour - boldness Signed-off-by: Lance Chant <13349722+lancechant@users.noreply.github.com> --- app/(auth)/player/direct-player.tsx | 76 ++++++- components/settings/SubtitleToggles.tsx | 207 ++++++++++++++++++++ components/settings/VLCSubtitleSettings.tsx | 156 +++++++++++++++ translations/en.json | 33 +++- 4 files changed, 470 insertions(+), 2 deletions(-) create mode 100644 components/settings/VLCSubtitleSettings.tsx diff --git a/app/(auth)/player/direct-player.tsx b/app/(auth)/player/direct-player.tsx index ca906a78..10135002 100644 --- a/app/(auth)/player/direct-player.tsx +++ b/app/(auth)/player/direct-player.tsx @@ -37,9 +37,29 @@ import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; import { useSettings } from "@/utils/atoms/settings"; import { getStreamUrl } from "@/utils/jellyfin/media/getStreamUrl"; import { writeToLog } from "@/utils/log"; +import { storage } from "@/utils/mmkv"; import { generateDeviceProfile } from "@/utils/profiles/native"; import { msToTicks, ticksToSeconds } from "@/utils/time"; +type VLCColor = + | "Black" + | "Gray" + | "Silver" + | "White" + | "Maroon" + | "Red" + | "Fuchsia" + | "Yellow" + | "Olive" + | "Green" + | "Teal" + | "Lime" + | "Purple" + | "Navy" + | "Blue" + | "Aqua"; +type OutlineThickness = "None" | "Thin" | "Normal" | "Thick"; + export default function page() { const videoRef = useRef(null); const user = useAtomValue(userAtom); @@ -576,8 +596,62 @@ export default function page() { ? allSubs.indexOf(chosenSubtitleTrack) : [...textSubs].reverse().indexOf(chosenSubtitleTrack); initOptions.push(`--sub-track=${finalIndex}`); - } + // Add VLC subtitle styling options from settings + const textColor = (storage.getString("vlc.textColor") || + "White") as VLCColor; + const backgroundColor = (storage.getString("vlc.backgroundColor") || + "Black") as VLCColor; + const outlineColor = (storage.getString("vlc.outlineColor") || + "Black") as VLCColor; + const outlineThickness = (storage.getString("vlc.outlineThickness") || + "Normal") as OutlineThickness; + const backgroundOpacity = storage.getNumber("vlc.backgroundOpacity") || 128; + const outlineOpacity = storage.getNumber("vlc.outlineOpacity") || 255; + const isBold = storage.getBoolean("vlc.isBold") || false; + + // VLC color values mapping + const VLC_COLORS: Record = { + Black: 0, + Gray: 8421504, + Silver: 12632256, + White: 16777215, + Maroon: 8388608, + Red: 16711680, + Fuchsia: 16711935, + Yellow: 16776960, + Olive: 8421376, + Green: 32768, + Teal: 32896, + Lime: 65280, + Purple: 8388736, + Navy: 128, + Blue: 255, + Aqua: 65535, + }; + + const OUTLINE_THICKNESS: Record = { + None: 0, + Thin: 2, + Normal: 4, + Thick: 6, + }; + + // Add subtitle styling options + initOptions.push(`--freetype-color=${VLC_COLORS[textColor]}`); + initOptions.push(`--freetype-background-opacity=${backgroundOpacity}`); + initOptions.push( + `--freetype-background-color=${VLC_COLORS[backgroundColor]}`, + ); + initOptions.push(`--freetype-outline-opacity=${outlineOpacity}`); + initOptions.push(`--freetype-outline-color=${VLC_COLORS[outlineColor]}`); + initOptions.push( + `--freetype-outline-thickness=${OUTLINE_THICKNESS[outlineThickness]}`, + ); + if (isBold) { + initOptions.push("--freetype-bold"); + } + } if (notTranscoding && chosenAudioTrack) { initOptions.push(`--audio-track=${allAudio.indexOf(chosenAudioTrack)}`); } diff --git a/components/settings/SubtitleToggles.tsx b/components/settings/SubtitleToggles.tsx index 59ec1570..e196a599 100644 --- a/components/settings/SubtitleToggles.tsx +++ b/components/settings/SubtitleToggles.tsx @@ -1,3 +1,4 @@ +import { useEffect, useState } from "react"; import { Platform, TouchableOpacity, View, type ViewProps } from "react-native"; const _DropdownMenu = !Platform.isTV ? require("zeego/dropdown-menu") : null; @@ -9,6 +10,7 @@ import { Switch } from "react-native-gesture-handler"; import Dropdown from "@/components/common/Dropdown"; import { Stepper } from "@/components/inputs/Stepper"; import { useSettings } from "@/utils/atoms/settings"; +import { storage } from "@/utils/mmkv"; import { Text } from "../common/Text"; import { ListGroup } from "../list/ListGroup"; import { ListItem } from "../list/ListItem"; @@ -16,6 +18,32 @@ import { useMedia } from "./MediaContext"; interface Props extends ViewProps {} +const VLC_COLORS = { + Black: 0, + Gray: 8421504, + Silver: 12632256, + White: 16777215, + Maroon: 8388608, + Red: 16711680, + Fuchsia: 16711935, + Yellow: 16776960, + Olive: 8421376, + Green: 32768, + Teal: 32896, + Lime: 65280, + Purple: 8388736, + Navy: 128, + Blue: 255, + Aqua: 65535, +}; + +const OUTLINE_THICKNESS = { + None: 0, + Thin: 2, + Normal: 4, + Thick: 6, +}; + export const SubtitleToggles: React.FC = ({ ...props }) => { const isTv = Platform.isTV; @@ -25,6 +53,52 @@ export const SubtitleToggles: React.FC = ({ ...props }) => { const cultures = media.cultures; const { t } = useTranslation(); + // VLC subtitle styling states + const [textColor, setTextColor] = useState( + storage.getString("vlc.textColor") || "White", + ); + const [backgroundColor, setBackgroundColor] = useState( + storage.getString("vlc.backgroundColor") || "Black", + ); + const [outlineColor, setOutlineColor] = useState( + storage.getString("vlc.outlineColor") || "Black", + ); + const [outlineThickness, setOutlineThickness] = useState( + storage.getString("vlc.outlineThickness") || "Normal", + ); + const [backgroundOpacity, setBackgroundOpacity] = useState( + storage.getNumber("vlc.backgroundOpacity") || 128, + ); + const [outlineOpacity, setOutlineOpacity] = useState( + storage.getNumber("vlc.outlineOpacity") || 255, + ); + const [isBold, setIsBold] = useState( + storage.getBoolean("vlc.isBold") || false, + ); + + // VLC settings effects + useEffect(() => { + storage.set("vlc.textColor", textColor); + }, [textColor]); + useEffect(() => { + storage.set("vlc.backgroundColor", backgroundColor); + }, [backgroundColor]); + useEffect(() => { + storage.set("vlc.outlineColor", outlineColor); + }, [outlineColor]); + useEffect(() => { + storage.set("vlc.outlineThickness", outlineThickness); + }, [outlineThickness]); + useEffect(() => { + storage.set("vlc.backgroundOpacity", backgroundOpacity); + }, [backgroundOpacity]); + useEffect(() => { + storage.set("vlc.outlineOpacity", outlineOpacity); + }, [outlineOpacity]); + useEffect(() => { + storage.set("vlc.isBold", isBold); + }, [isBold]); + if (isTv) return null; if (!settings) return null; @@ -147,6 +221,139 @@ export const SubtitleToggles: React.FC = ({ ...props }) => { onUpdate={(subtitleSize) => updateSettings({ subtitleSize })} /> + + item} + titleExtractor={(item) => + t(`home.settings.subtitles.colors.${item}`) + } + title={ + + + {t(`home.settings.subtitles.colors.${textColor}`)} + + + + } + label={t("home.settings.subtitles.text_color")} + onSelected={setTextColor} + /> + + + item} + titleExtractor={(item) => + t(`home.settings.subtitles.colors.${item}`) + } + title={ + + + {t(`home.settings.subtitles.colors.${backgroundColor}`)} + + + + } + label={t("home.settings.subtitles.background_color")} + onSelected={setBackgroundColor} + /> + + + item} + titleExtractor={(item) => + t(`home.settings.subtitles.colors.${item}`) + } + title={ + + + {t(`home.settings.subtitles.colors.${outlineColor}`)} + + + + } + label={t("home.settings.subtitles.outline_color")} + onSelected={setOutlineColor} + /> + + + item} + titleExtractor={(item) => + t(`home.settings.subtitles.thickness.${item}`) + } + title={ + + + {t(`home.settings.subtitles.thickness.${outlineThickness}`)} + + + + } + label={t("home.settings.subtitles.outline_thickness")} + onSelected={setOutlineThickness} + /> + + + `${Math.round((item / 255) * 100)}%`} + title={ + + {`${Math.round((backgroundOpacity / 255) * 100)}%`} + + + } + label={t("home.settings.subtitles.background_opacity")} + onSelected={setBackgroundOpacity} + /> + + + `${Math.round((item / 255) * 100)}%`} + title={ + + {`${Math.round((outlineOpacity / 255) * 100)}%`} + + + } + label={t("home.settings.subtitles.outline_opacity")} + onSelected={setOutlineOpacity} + /> + + + + ); diff --git a/components/settings/VLCSubtitleSettings.tsx b/components/settings/VLCSubtitleSettings.tsx new file mode 100644 index 00000000..919ee14e --- /dev/null +++ b/components/settings/VLCSubtitleSettings.tsx @@ -0,0 +1,156 @@ +import { t } from "i18next"; +import { useEffect, useState } from "react"; +import { View } from "react-native"; +import { ListGroup } from "@/components/list/ListGroup"; +import { ListItem } from "@/components/list/ListItem"; +import { storage } from "@/utils/mmkv"; + +const VLC_COLORS = { + Black: 0, + Gray: 8421504, + Silver: 12632256, + White: 16777215, + Maroon: 8388608, + Red: 16711680, + Fuchsia: 16711935, + Yellow: 16776960, + Olive: 8421376, + Green: 32768, + Teal: 32896, + Lime: 65280, + Purple: 8388736, + Navy: 128, + Blue: 255, + Aqua: 65535, +}; + +const OUTLINE_THICKNESS = { + None: 0, + Thin: 2, + Normal: 4, + Thick: 6, +}; + +export function VLCSubtitleSettings({ + className = "", +}: { + className?: string; +}) { + const [textColor, setTextColor] = useState( + storage.getString("vlc.textColor") || "White", + ); + const [backgroundColor, setBackgroundColor] = useState( + storage.getString("vlc.backgroundColor") || "Black", + ); + const [outlineColor, setOutlineColor] = useState( + storage.getString("vlc.outlineColor") || "Black", + ); + const [outlineThickness, setOutlineThickness] = useState( + storage.getString("vlc.outlineThickness") || "Normal", + ); + const [backgroundOpacity, setBackgroundOpacity] = useState( + storage.getNumber("vlc.backgroundOpacity") || 128, + ); + const [outlineOpacity, setOutlineOpacity] = useState( + storage.getNumber("vlc.outlineOpacity") || 255, + ); + const [isBold, setIsBold] = useState( + storage.getBoolean("vlc.isBold") || false, + ); + + useEffect(() => { + storage.set("vlc.textColor", textColor); + }, [textColor]); + + useEffect(() => { + storage.set("vlc.backgroundColor", backgroundColor); + }, [backgroundColor]); + + useEffect(() => { + storage.set("vlc.outlineColor", outlineColor); + }, [outlineColor]); + + useEffect(() => { + storage.set("vlc.outlineThickness", outlineThickness); + }, [outlineThickness]); + + useEffect(() => { + storage.set("vlc.backgroundOpacity", backgroundOpacity); + }, [backgroundOpacity]); + + useEffect(() => { + storage.set("vlc.outlineOpacity", outlineOpacity); + }, [outlineOpacity]); + + useEffect(() => { + storage.set("vlc.isBold", isBold); + }, [isBold]); + + return ( + + + { + const colors = Object.keys(VLC_COLORS); + const currentIndex = colors.indexOf(textColor); + const nextIndex = (currentIndex + 1) % colors.length; + setTextColor(colors[nextIndex]); + }} + /> + { + const colors = Object.keys(VLC_COLORS); + const currentIndex = colors.indexOf(backgroundColor); + const nextIndex = (currentIndex + 1) % colors.length; + setBackgroundColor(colors[nextIndex]); + }} + /> + { + const colors = Object.keys(VLC_COLORS); + const currentIndex = colors.indexOf(outlineColor); + const nextIndex = (currentIndex + 1) % colors.length; + setOutlineColor(colors[nextIndex]); + }} + /> + { + const thicknesses = Object.keys(OUTLINE_THICKNESS); + const currentIndex = thicknesses.indexOf(outlineThickness); + const nextIndex = (currentIndex + 1) % thicknesses.length; + setOutlineThickness(thicknesses[nextIndex]); + }} + /> + { + const newOpacity = (backgroundOpacity + 32) % 256; + setBackgroundOpacity(newOpacity); + }} + /> + { + const newOpacity = (outlineOpacity + 32) % 256; + setOutlineOpacity(newOpacity); + }} + /> + setIsBold(!isBold)} + /> + + + ); +} diff --git a/translations/en.json b/translations/en.json index b1ccbb80..af6caf18 100644 --- a/translations/en.json +++ b/translations/en.json @@ -106,11 +106,11 @@ }, "subtitles": { "subtitle_title": "Subtitles", + "subtitle_hint": "Configure how subtitles look and behave.", "subtitle_language": "Subtitle language", "subtitle_mode": "Subtitle Mode", "set_subtitle_track": "Set Subtitle Track From Previous Item", "subtitle_size": "Subtitle Size", - "subtitle_hint": "Configure subtitle preference.", "none": "None", "language": "Language", "loading": "Loading", @@ -120,6 +120,37 @@ "Always": "Always", "None": "None", "OnlyForced": "OnlyForced" + }, + "text_color": "Text Color", + "background_color": "Background Color", + "outline_color": "Outline Color", + "outline_thickness": "Outline Thickness", + "background_opacity": "Background Opacity", + "outline_opacity": "Outline Opacity", + "bold_text": "Bold Text", + "colors": { + "Black": "Black", + "Gray": "Gray", + "Silver": "Silver", + "White": "White", + "Maroon": "Maroon", + "Red": "Red", + "Fuchsia": "Fuchsia", + "Yellow": "Yellow", + "Olive": "Olive", + "Green": "Green", + "Teal": "Teal", + "Lime": "Lime", + "Purple": "Purple", + "Navy": "Navy", + "Blue": "Blue", + "Aqua": "Aqua" + }, + "thickness": { + "None": "None", + "Thin": "Thin", + "Normal": "Normal", + "Thick": "Thick" } }, "other": { From 388f65b44335ebaadd42c471b7dd1fd12cccae9c Mon Sep 17 00:00:00 2001 From: Lance Chant <13349722+lancechant@users.noreply.github.com> Date: Sat, 20 Sep 2025 13:03:07 +0200 Subject: [PATCH 02/29] chore: moved constant values to a file Reduced duplication and removed constants to a new file Signed-off-by: Lance Chant <13349722+lancechant@users.noreply.github.com> --- app/(auth)/player/direct-player.tsx | 52 +++------------------ components/settings/StorageSettings.tsx | 1 - components/settings/SubtitleToggles.tsx | 26 +---------- components/settings/VLCSubtitleSettings.tsx | 27 +---------- constants/SubtitleConstants.ts | 45 ++++++++++++++++++ 5 files changed, 53 insertions(+), 98 deletions(-) create mode 100644 constants/SubtitleConstants.ts diff --git a/app/(auth)/player/direct-player.tsx b/app/(auth)/player/direct-player.tsx index 10135002..7f310ec8 100644 --- a/app/(auth)/player/direct-player.tsx +++ b/app/(auth)/player/direct-player.tsx @@ -21,6 +21,12 @@ import { BITRATES } from "@/components/BitrateSelector"; import { Text } from "@/components/common/Text"; import { Loader } from "@/components/Loader"; import { Controls } from "@/components/video-player/controls/Controls"; +import { + OUTLINE_THICKNESS, + OutlineThickness, + VLC_COLORS, + VLCColor, +} from "@/constants/SubtitleConstants"; import { useHaptic } from "@/hooks/useHaptic"; import { usePlaybackManager } from "@/hooks/usePlaybackManager"; import { useInvalidatePlaybackProgressCache } from "@/hooks/useRevalidatePlaybackProgressCache"; @@ -41,25 +47,6 @@ import { storage } from "@/utils/mmkv"; import { generateDeviceProfile } from "@/utils/profiles/native"; import { msToTicks, ticksToSeconds } from "@/utils/time"; -type VLCColor = - | "Black" - | "Gray" - | "Silver" - | "White" - | "Maroon" - | "Red" - | "Fuchsia" - | "Yellow" - | "Olive" - | "Green" - | "Teal" - | "Lime" - | "Purple" - | "Navy" - | "Blue" - | "Aqua"; -type OutlineThickness = "None" | "Thin" | "Normal" | "Thick"; - export default function page() { const videoRef = useRef(null); const user = useAtomValue(userAtom); @@ -610,33 +597,6 @@ export default function page() { const outlineOpacity = storage.getNumber("vlc.outlineOpacity") || 255; const isBold = storage.getBoolean("vlc.isBold") || false; - // VLC color values mapping - const VLC_COLORS: Record = { - Black: 0, - Gray: 8421504, - Silver: 12632256, - White: 16777215, - Maroon: 8388608, - Red: 16711680, - Fuchsia: 16711935, - Yellow: 16776960, - Olive: 8421376, - Green: 32768, - Teal: 32896, - Lime: 65280, - Purple: 8388736, - Navy: 128, - Blue: 255, - Aqua: 65535, - }; - - const OUTLINE_THICKNESS: Record = { - None: 0, - Thin: 2, - Normal: 4, - Thick: 6, - }; - // Add subtitle styling options initOptions.push(`--freetype-color=${VLC_COLORS[textColor]}`); initOptions.push(`--freetype-background-opacity=${backgroundOpacity}`); diff --git a/components/settings/StorageSettings.tsx b/components/settings/StorageSettings.tsx index 89f6a2e9..117152fc 100644 --- a/components/settings/StorageSettings.tsx +++ b/components/settings/StorageSettings.tsx @@ -40,7 +40,6 @@ export const StorageSettings = () => { }; const calculatePercentage = (value: number, total: number) => { - console.log("usage", value, total); return ((value / total) * 100).toFixed(2); }; diff --git a/components/settings/SubtitleToggles.tsx b/components/settings/SubtitleToggles.tsx index e196a599..d1d30274 100644 --- a/components/settings/SubtitleToggles.tsx +++ b/components/settings/SubtitleToggles.tsx @@ -18,31 +18,7 @@ import { useMedia } from "./MediaContext"; interface Props extends ViewProps {} -const VLC_COLORS = { - Black: 0, - Gray: 8421504, - Silver: 12632256, - White: 16777215, - Maroon: 8388608, - Red: 16711680, - Fuchsia: 16711935, - Yellow: 16776960, - Olive: 8421376, - Green: 32768, - Teal: 32896, - Lime: 65280, - Purple: 8388736, - Navy: 128, - Blue: 255, - Aqua: 65535, -}; - -const OUTLINE_THICKNESS = { - None: 0, - Thin: 2, - Normal: 4, - Thick: 6, -}; +import { OUTLINE_THICKNESS, VLC_COLORS } from "@/constants/SubtitleConstants"; export const SubtitleToggles: React.FC = ({ ...props }) => { const isTv = Platform.isTV; diff --git a/components/settings/VLCSubtitleSettings.tsx b/components/settings/VLCSubtitleSettings.tsx index 919ee14e..4596f781 100644 --- a/components/settings/VLCSubtitleSettings.tsx +++ b/components/settings/VLCSubtitleSettings.tsx @@ -3,34 +3,9 @@ import { useEffect, useState } from "react"; import { View } from "react-native"; import { ListGroup } from "@/components/list/ListGroup"; import { ListItem } from "@/components/list/ListItem"; +import { OUTLINE_THICKNESS, VLC_COLORS } from "@/constants/SubtitleConstants"; import { storage } from "@/utils/mmkv"; -const VLC_COLORS = { - Black: 0, - Gray: 8421504, - Silver: 12632256, - White: 16777215, - Maroon: 8388608, - Red: 16711680, - Fuchsia: 16711935, - Yellow: 16776960, - Olive: 8421376, - Green: 32768, - Teal: 32896, - Lime: 65280, - Purple: 8388736, - Navy: 128, - Blue: 255, - Aqua: 65535, -}; - -const OUTLINE_THICKNESS = { - None: 0, - Thin: 2, - Normal: 4, - Thick: 6, -}; - export function VLCSubtitleSettings({ className = "", }: { diff --git a/constants/SubtitleConstants.ts b/constants/SubtitleConstants.ts new file mode 100644 index 00000000..7fc7a8e6 --- /dev/null +++ b/constants/SubtitleConstants.ts @@ -0,0 +1,45 @@ +export type VLCColor = + | "Black" + | "Gray" + | "Silver" + | "White" + | "Maroon" + | "Red" + | "Fuchsia" + | "Yellow" + | "Olive" + | "Green" + | "Teal" + | "Lime" + | "Purple" + | "Navy" + | "Blue" + | "Aqua"; + +export type OutlineThickness = "None" | "Thin" | "Normal" | "Thick"; + +export const VLC_COLORS: Record = { + Black: 0, + Gray: 8421504, + Silver: 12632256, + White: 16777215, + Maroon: 8388608, + Red: 16711680, + Fuchsia: 16711935, + Yellow: 16776960, + Olive: 8421376, + Green: 32768, + Teal: 32896, + Lime: 65280, + Purple: 8388736, + Navy: 128, + Blue: 255, + Aqua: 65535, +}; + +export const OUTLINE_THICKNESS: Record = { + None: 0, + Thin: 2, + Normal: 4, + Thick: 6, +}; From 28f6729ae2f0f4c436e0a22577470d86946100ca Mon Sep 17 00:00:00 2001 From: lance chant <13349722+lancechant@users.noreply.github.com> Date: Thu, 25 Sep 2025 07:58:36 +0200 Subject: [PATCH 03/29] Update direct-player.tsx Added text scaling --- app/(auth)/player/direct-player.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/(auth)/player/direct-player.tsx b/app/(auth)/player/direct-player.tsx index d0f2885b..1200abc2 100644 --- a/app/(auth)/player/direct-player.tsx +++ b/app/(auth)/player/direct-player.tsx @@ -105,7 +105,7 @@ export default function page() { /** Playback position in ticks. */ playbackPosition?: string; }>(); - useSettings(); + const { settings } = useSettings(); const offline = offlineStr === "true"; const playbackManager = usePlaybackManager(); @@ -589,6 +589,8 @@ export default function page() { initOptions.push( `--freetype-outline-thickness=${OUTLINE_THICKNESS[outlineThickness]}`, ); + initOptions.push(`--sub-text-scale=${settings.subtitleSize}`), + initOptions.push("--sub-margin=40"), if (isBold) { initOptions.push("--freetype-bold"); } From d250295e3610643dded7eb721b88a621f15e69e3 Mon Sep 17 00:00:00 2001 From: lance chant <13349722+lancechant@users.noreply.github.com> Date: Thu, 25 Sep 2025 09:11:24 +0200 Subject: [PATCH 04/29] Update direct-player.tsx Fixed syntax --- app/(auth)/player/direct-player.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/(auth)/player/direct-player.tsx b/app/(auth)/player/direct-player.tsx index 1200abc2..a8caf201 100644 --- a/app/(auth)/player/direct-player.tsx +++ b/app/(auth)/player/direct-player.tsx @@ -589,8 +589,8 @@ export default function page() { initOptions.push( `--freetype-outline-thickness=${OUTLINE_THICKNESS[outlineThickness]}`, ); - initOptions.push(`--sub-text-scale=${settings.subtitleSize}`), - initOptions.push("--sub-margin=40"), + initOptions.push(`--sub-text-scale=${settings.subtitleSize}`); + initOptions.push("--sub-margin=40"); if (isBold) { initOptions.push("--freetype-bold"); } From f104e952ab0b7ffc45483065e5ab014b53740285 Mon Sep 17 00:00:00 2001 From: Gauvain <68083474+Gauvino@users.noreply.github.com> Date: Mon, 29 Sep 2025 18:37:02 +0200 Subject: [PATCH 05/29] feat: add automated PR comments for build artifacts (#1099) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .github/workflows/artifact-comment.yml | 124 +++++++++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 .github/workflows/artifact-comment.yml diff --git a/.github/workflows/artifact-comment.yml b/.github/workflows/artifact-comment.yml new file mode 100644 index 00000000..112f7113 --- /dev/null +++ b/.github/workflows/artifact-comment.yml @@ -0,0 +1,124 @@ +name: 📝 Artifact Comment on PR + +concurrency: + group: artifact-comment-${{ github.event.workflow_run.head_branch }} + cancel-in-progress: true + +on: + workflow_run: + workflows: ["đŸ€– iOS IPA Build (Phone + TV)", "đŸ€– Android APK Build (Phone + TV)"] + types: + - completed + +jobs: + comment-artifacts: + if: github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success' + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + actions: read + + steps: + - name: 🔍 Get PR and Artifacts + uses: actions/github-script@v8 + with: + script: | + // Find PR associated with this commit + const { data: pullRequests } = await github.rest.repos.listPullRequestsAssociatedWithCommit({ + owner: context.repo.owner, + repo: context.repo.repo, + commit_sha: "${{ github.event.workflow_run.head_sha }}" + }); + + if (pullRequests.length === 0) { + core.setFailed('No pull request found for this commit'); + return; + } + + const pr = pullRequests[0]; + const runId = "${{ github.event.workflow_run.id }}"; + + // Get artifacts from the workflow run + const { data: artifacts } = await github.rest.actions.listWorkflowRunArtifacts({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: runId + }); + + if (artifacts.artifacts.length === 0) { + console.log('No artifacts found for this run'); + return; + } + + // Sort and categorize artifacts + const androidArtifacts = artifacts.artifacts + .filter(a => a.name.includes('android')) + .sort((a, b) => a.name.localeCompare(b.name)); + const iosArtifacts = artifacts.artifacts + .filter(a => a.name.includes('ios')) + .sort((a, b) => a.name.localeCompare(b.name)); + + // Build comment body with table format + let commentBody = `## đŸ“± Build Artifacts Ready!\n\n`; + commentBody += `✅ **Workflow completed successfully** for PR #${pr.number}\n`; + commentBody += `📩 **${artifacts.artifacts.length} artifacts** generated from commit [\`${pr.head.sha.substring(0, 7)}\`](https://github.com/${context.repo.owner}/${context.repo.repo}/commit/${pr.head.sha})\n\n`; + + // Create table for better organization + commentBody += `| Platform | Device Type | Download Link |\n`; + commentBody += `|----------|-------------|---------------|\n`; + + // Add Android artifacts + androidArtifacts.forEach(artifact => { + const isTV = artifact.name.includes('tv'); + const deviceType = isTV ? 'đŸ“ș Android TV' : 'đŸ“± Android Phone'; + const nightlyLink = `https://nightly.link/${context.repo.owner}/${context.repo.repo}/actions/runs/${runId}/${artifact.name}.zip`; + commentBody += `| đŸ€– Android | ${deviceType} | [đŸ“„ Download APK](${nightlyLink}) |\n`; + }); + + // Add iOS artifacts + iosArtifacts.forEach(artifact => { + const isTV = artifact.name.includes('tv'); + const deviceType = isTV ? 'đŸ“ș Apple TV' : 'đŸ“± iPhone/iPad'; + const nightlyLink = `https://nightly.link/${context.repo.owner}/${context.repo.repo}/actions/runs/${runId}/${artifact.name}.zip`; + commentBody += `| 🍎 iOS | ${deviceType} | [đŸ“„ Download IPA](${nightlyLink}) |\n`; + }); + + commentBody += `\n`; + commentBody += `### 🔧 Installation Instructions\n\n`; + commentBody += `- **Android APK**: Download and install directly on your device (enable "Install from unknown sources")\n`; + commentBody += `- **iOS IPA**: Install using [AltStore](https://altstore.io/), [Sideloadly](https://sideloadly.io/), or Xcode\n\n`; + commentBody += `> ⚠ **Note**: Artifacts expire in 7 days from build date\n\n`; + commentBody += `*Auto-generated by [GitHub Actions](${context.payload.workflow_run.html_url})*`; + + // Find existing bot comment to update + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pr.number + }); + + const botComment = comments.find(comment => + comment.user.type === 'Bot' && + comment.body.includes('Build Artifacts Ready!') + ); + + if (botComment) { + // Update existing comment + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: botComment.id, + body: commentBody + }); + console.log(`✅ Updated comment ${botComment.id} on PR #${pr.number}`); + } else { + // Create new comment + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pr.number, + body: commentBody + }); + console.log(`✅ Created new comment on PR #${pr.number}`); + } From 7fe3ca84842c292b612e7902fe7c6ab0ea06a6d1 Mon Sep 17 00:00:00 2001 From: Gauvain <68083474+Gauvino@users.noreply.github.com> Date: Mon, 29 Sep 2025 22:31:41 +0200 Subject: [PATCH 06/29] feat: add automated PR comments for build artifacts (#1100) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .github/workflows/artifact-comment.yml | 103 +++++++++++++++---------- 1 file changed, 61 insertions(+), 42 deletions(-) diff --git a/.github/workflows/artifact-comment.yml b/.github/workflows/artifact-comment.yml index 112f7113..67e31268 100644 --- a/.github/workflows/artifact-comment.yml +++ b/.github/workflows/artifact-comment.yml @@ -1,12 +1,17 @@ name: 📝 Artifact Comment on PR concurrency: - group: artifact-comment-${{ github.event.workflow_run.head_branch }} - cancel-in-progress: true + group: artifact-comment-${{ github.event.workflow_run.id }} + cancel-in-progress: false on: + workflow_dispatch: workflow_run: - workflows: ["đŸ€– iOS IPA Build (Phone + TV)", "đŸ€– Android APK Build (Phone + TV)"] + workflows: + - "đŸ—ïž Build iOS IPA (phone)" + - "đŸ—ïž Build iOS IPA (tv)" + - "đŸ—ïž Build Android APK (phone)" + - "đŸ—ïž Build Android APK (tv)" types: - completed @@ -24,33 +29,39 @@ jobs: uses: actions/github-script@v8 with: script: | + console.log('Workflow run event:', JSON.stringify(github.event.workflow_run, null, 2)); + // Find PR associated with this commit const { data: pullRequests } = await github.rest.repos.listPullRequestsAssociatedWithCommit({ owner: context.repo.owner, repo: context.repo.repo, commit_sha: "${{ github.event.workflow_run.head_sha }}" }); - + if (pullRequests.length === 0) { - core.setFailed('No pull request found for this commit'); + console.log('No pull request found for commit: ${{ github.event.workflow_run.head_sha }}'); return; } - + const pr = pullRequests[0]; const runId = "${{ github.event.workflow_run.id }}"; - + + console.log(`Found PR #${pr.number} for commit ${pr.head.sha.substring(0, 7)}`); + // Get artifacts from the workflow run const { data: artifacts } = await github.rest.actions.listWorkflowRunArtifacts({ owner: context.repo.owner, repo: context.repo.repo, run_id: runId }); - - if (artifacts.artifacts.length === 0) { + + console.log(`Found ${artifacts?.artifacts?.length || 0} artifacts`); + + if (!artifacts || artifacts.artifacts.length === 0) { console.log('No artifacts found for this run'); return; } - + // Sort and categorize artifacts const androidArtifacts = artifacts.artifacts .filter(a => a.name.includes('android')) @@ -58,51 +69,59 @@ jobs: const iosArtifacts = artifacts.artifacts .filter(a => a.name.includes('ios')) .sort((a, b) => a.name.localeCompare(b.name)); - + + console.log(`Android artifacts: ${androidArtifacts.length}, iOS artifacts: ${iosArtifacts.length}`); + // Build comment body with table format let commentBody = `## đŸ“± Build Artifacts Ready!\n\n`; commentBody += `✅ **Workflow completed successfully** for PR #${pr.number}\n`; commentBody += `📩 **${artifacts.artifacts.length} artifacts** generated from commit [\`${pr.head.sha.substring(0, 7)}\`](https://github.com/${context.repo.owner}/${context.repo.repo}/commit/${pr.head.sha})\n\n`; - - // Create table for better organization - commentBody += `| Platform | Device Type | Download Link |\n`; - commentBody += `|----------|-------------|---------------|\n`; - - // Add Android artifacts - androidArtifacts.forEach(artifact => { - const isTV = artifact.name.includes('tv'); - const deviceType = isTV ? 'đŸ“ș Android TV' : 'đŸ“± Android Phone'; - const nightlyLink = `https://nightly.link/${context.repo.owner}/${context.repo.repo}/actions/runs/${runId}/${artifact.name}.zip`; - commentBody += `| đŸ€– Android | ${deviceType} | [đŸ“„ Download APK](${nightlyLink}) |\n`; - }); - - // Add iOS artifacts - iosArtifacts.forEach(artifact => { - const isTV = artifact.name.includes('tv'); - const deviceType = isTV ? 'đŸ“ș Apple TV' : 'đŸ“± iPhone/iPad'; - const nightlyLink = `https://nightly.link/${context.repo.owner}/${context.repo.repo}/actions/runs/${runId}/${artifact.name}.zip`; - commentBody += `| 🍎 iOS | ${deviceType} | [đŸ“„ Download IPA](${nightlyLink}) |\n`; - }); - - commentBody += `\n`; - commentBody += `### 🔧 Installation Instructions\n\n`; - commentBody += `- **Android APK**: Download and install directly on your device (enable "Install from unknown sources")\n`; - commentBody += `- **iOS IPA**: Install using [AltStore](https://altstore.io/), [Sideloadly](https://sideloadly.io/), or Xcode\n\n`; - commentBody += `> ⚠ **Note**: Artifacts expire in 7 days from build date\n\n`; - commentBody += `*Auto-generated by [GitHub Actions](${context.payload.workflow_run.html_url})*`; - + + if (androidArtifacts.length === 0 && iosArtifacts.length === 0) { + commentBody += `⚠ No mobile app artifacts found in this build.\n\n`; + } else { + // Create table for better organization + commentBody += `| Platform | Device Type | Download Link |\n`; + commentBody += `|----------|-------------|---------------|\n`; + + // Add Android artifacts + androidArtifacts.forEach(artifact => { + const isTV = artifact.name.includes('tv'); + const deviceType = isTV ? 'đŸ“ș Android TV' : 'đŸ“± Android Phone'; + const nightlyLink = `https://nightly.link/${context.repo.owner}/${context.repo.repo}/actions/runs/${runId}/${artifact.name}.zip`; + commentBody += `| đŸ€– Android | ${deviceType} | [đŸ“„ Download APK](${nightlyLink}) |\n`; + }); + + // Add iOS artifacts + iosArtifacts.forEach(artifact => { + const isTV = artifact.name.includes('tv'); + const deviceType = isTV ? 'đŸ“ș Apple TV' : 'đŸ“± iPhone/iPad'; + const nightlyLink = `https://nightly.link/${context.repo.owner}/${context.repo.repo}/actions/runs/${runId}/${artifact.name}.zip`; + commentBody += `| 🍎 iOS | ${deviceType} | [đŸ“„ Download IPA](${nightlyLink}) |\n`; + }); + + commentBody += `\n`; + commentBody += `### 🔧 Installation Instructions\n\n`; + commentBody += `- **Android APK**: Download and install directly on your device (enable "Install from unknown sources")\n`; + commentBody += `- **iOS IPA**: Install using [AltStore](https://altstore.io/), [Sideloadly](https://sideloadly.io/), or Xcode\n\n`; + commentBody += `> ⚠ **Note**: Artifacts expire in 7 days from build date\n\n`; + } + + commentBody += `*Auto-generated by [GitHub Actions](https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${runId})*`; + commentBody += `\n`; + // Find existing bot comment to update const { data: comments } = await github.rest.issues.listComments({ owner: context.repo.owner, repo: context.repo.repo, issue_number: pr.number }); - + const botComment = comments.find(comment => comment.user.type === 'Bot' && - comment.body.includes('Build Artifacts Ready!') + comment.body.includes('') ); - + if (botComment) { // Update existing comment await github.rest.issues.updateComment({ From 92e40c7aa0d1ca4ddff4cbfce786f9c080dea2a9 Mon Sep 17 00:00:00 2001 From: Gauvain <68083474+Gauvino@users.noreply.github.com> Date: Mon, 29 Sep 2025 23:16:12 +0200 Subject: [PATCH 07/29] feat: Build in pr (#1101) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .github/workflows/artifact-comment.yml | 75 +++++++++++++++++--------- 1 file changed, 51 insertions(+), 24 deletions(-) diff --git a/.github/workflows/artifact-comment.yml b/.github/workflows/artifact-comment.yml index 67e31268..14e74800 100644 --- a/.github/workflows/artifact-comment.yml +++ b/.github/workflows/artifact-comment.yml @@ -1,23 +1,21 @@ name: 📝 Artifact Comment on PR concurrency: - group: artifact-comment-${{ github.event.workflow_run.id }} + group: artifact-comment-${{ github.event.workflow_run.id || github.run_id }} cancel-in-progress: false on: - workflow_dispatch: + workflow_dispatch: # Allow manual testing workflow_run: workflows: - - "đŸ—ïž Build iOS IPA (phone)" - - "đŸ—ïž Build iOS IPA (tv)" - - "đŸ—ïž Build Android APK (phone)" - - "đŸ—ïž Build Android APK (tv)" + - "đŸ€– Android APK Build (Phone + TV)" + - "đŸ€– iOS IPA Build (Phone + TV)" types: - completed jobs: comment-artifacts: - if: github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success' + if: github.event_name == 'workflow_dispatch' || (github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success') runs-on: ubuntu-latest permissions: contents: read @@ -29,23 +27,56 @@ jobs: uses: actions/github-script@v8 with: script: | - console.log('Workflow run event:', JSON.stringify(github.event.workflow_run, null, 2)); + // Handle both workflow_run and manual dispatch events + let runId, pr; - // Find PR associated with this commit - const { data: pullRequests } = await github.rest.repos.listPullRequestsAssociatedWithCommit({ - owner: context.repo.owner, - repo: context.repo.repo, - commit_sha: "${{ github.event.workflow_run.head_sha }}" - }); - - if (pullRequests.length === 0) { - console.log('No pull request found for commit: ${{ github.event.workflow_run.head_sha }}'); + if (context.eventName === 'workflow_run') { + runId = github.event.workflow_run.id; + + // Find PR associated with this commit + const { data: pullRequests } = await github.rest.repos.listPullRequestsAssociatedWithCommit({ + owner: context.repo.owner, + repo: context.repo.repo, + commit_sha: github.event.workflow_run.head_sha + }); + + if (pullRequests.length === 0) { + console.log('No pull request found for commit:', github.event.workflow_run.head_sha); + return; + } + pr = pullRequests[0]; + + } else if (context.eventName === 'workflow_dispatch') { + // For manual testing, use most recent test workflow run + const { data: workflows } = await github.rest.actions.listWorkflowRuns({ + owner: context.repo.owner, + repo: context.repo.repo, + workflow_id: 'test-artifact.yml', + per_page: 1 + }); + + if (workflows.workflow_runs.length === 0) { + console.log('No test workflow runs found'); + return; + } + + const testRun = workflows.workflow_runs[0]; + runId = testRun.id; + + // Get current PR for manual testing + const prNumber = context.payload.pull_request?.number || 1101; + const { data: prData } = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: prNumber + }); + pr = prData; + + } else { + console.log('Unsupported event type:', context.eventName); return; } - const pr = pullRequests[0]; - const runId = "${{ github.event.workflow_run.id }}"; - console.log(`Found PR #${pr.number} for commit ${pr.head.sha.substring(0, 7)}`); // Get artifacts from the workflow run @@ -55,8 +86,6 @@ jobs: run_id: runId }); - console.log(`Found ${artifacts?.artifacts?.length || 0} artifacts`); - if (!artifacts || artifacts.artifacts.length === 0) { console.log('No artifacts found for this run'); return; @@ -70,8 +99,6 @@ jobs: .filter(a => a.name.includes('ios')) .sort((a, b) => a.name.localeCompare(b.name)); - console.log(`Android artifacts: ${androidArtifacts.length}, iOS artifacts: ${iosArtifacts.length}`); - // Build comment body with table format let commentBody = `## đŸ“± Build Artifacts Ready!\n\n`; commentBody += `✅ **Workflow completed successfully** for PR #${pr.number}\n`; From 1ff09a2d34e7ac2e71c749deaef0ce8cf8d258fe Mon Sep 17 00:00:00 2001 From: Uruk Date: Mon, 29 Sep 2025 23:51:03 +0200 Subject: [PATCH 08/29] feat: enhance artifact workflow to show real-time build status Improves the artifact comment workflow to provide better visibility into ongoing builds by: - Triggering comments when builds start (requested event) instead of only on completion - Using commit SHA for concurrency grouping to better handle multiple builds for the same commit - Collecting artifacts from all recent build workflows for a PR rather than just the current run - Adding a build status table showing progress of Android and iOS workflows - Displaying progressive status updates even when builds are still in progress - Enabling cancel-in-progress to prevent redundant workflow runs This provides users with immediate feedback on build progress and comprehensive artifact availability across all platforms. --- .github/workflows/artifact-comment.yml | 128 +++++++++++++++++-------- 1 file changed, 87 insertions(+), 41 deletions(-) diff --git a/.github/workflows/artifact-comment.yml b/.github/workflows/artifact-comment.yml index 14e74800..60244409 100644 --- a/.github/workflows/artifact-comment.yml +++ b/.github/workflows/artifact-comment.yml @@ -1,8 +1,8 @@ name: 📝 Artifact Comment on PR concurrency: - group: artifact-comment-${{ github.event.workflow_run.id || github.run_id }} - cancel-in-progress: false + group: artifact-comment-${{ github.event.workflow_run.head_sha || github.sha }} + cancel-in-progress: true on: workflow_dispatch: # Allow manual testing @@ -12,10 +12,11 @@ on: - "đŸ€– iOS IPA Build (Phone + TV)" types: - completed + - requested # Trigger when build starts jobs: comment-artifacts: - if: github.event_name == 'workflow_dispatch' || (github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success') + if: github.event_name == 'workflow_dispatch' || github.event.workflow_run.event == 'pull_request' runs-on: ubuntu-latest permissions: contents: read @@ -28,11 +29,9 @@ jobs: with: script: | // Handle both workflow_run and manual dispatch events - let runId, pr; + let pr; if (context.eventName === 'workflow_run') { - runId = github.event.workflow_run.id; - // Find PR associated with this commit const { data: pullRequests } = await github.rest.repos.listPullRequestsAssociatedWithCommit({ owner: context.repo.owner, @@ -47,22 +46,6 @@ jobs: pr = pullRequests[0]; } else if (context.eventName === 'workflow_dispatch') { - // For manual testing, use most recent test workflow run - const { data: workflows } = await github.rest.actions.listWorkflowRuns({ - owner: context.repo.owner, - repo: context.repo.repo, - workflow_id: 'test-artifact.yml', - per_page: 1 - }); - - if (workflows.workflow_runs.length === 0) { - console.log('No test workflow runs found'); - return; - } - - const testRun = workflows.workflow_runs[0]; - runId = testRun.id; - // Get current PR for manual testing const prNumber = context.payload.pull_request?.number || 1101; const { data: prData } = await github.rest.pulls.get({ @@ -77,36 +60,97 @@ jobs: return; } - console.log(`Found PR #${pr.number} for commit ${pr.head.sha.substring(0, 7)}`); + console.log(`Processing PR #${pr.number} for commit ${pr.head.sha.substring(0, 7)}`); - // Get artifacts from the workflow run + // Get all recent workflow runs for this PR to collect artifacts from multiple builds + const { data: workflowRuns } = await github.rest.actions.listWorkflowRuns({ + owner: context.repo.owner, + repo: context.repo.repo, + head_sha: pr.head.sha, + per_page: 10 + }); + + // Filter for build workflows only + const buildRuns = workflowRuns.workflow_runs.filter(run => + run.name.includes('Android APK Build') || + run.name.includes('iOS IPA Build') + ); + + console.log(`Found ${buildRuns.length} build workflow runs for this commit`); + + // Collect artifacts from all completed successful builds + let allArtifacts = []; + let buildStatuses = {}; + + for (const run of buildRuns) { + buildStatuses[run.name] = { + status: run.status, + conclusion: run.conclusion, + url: run.html_url, + runId: run.id + }; + + if (run.conclusion === 'success') { + try { + const { data: artifacts } = await github.rest.actions.listWorkflowRunArtifacts({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: run.id + }); + allArtifacts.push(...artifacts.artifacts); + } catch (error) { + console.log(`Failed to get artifacts for run ${run.id}:`, error.message); + } + } + } + + console.log(`Collected ${allArtifacts.length} total artifacts from all builds`); + + // Get artifacts from current run if needed const { data: artifacts } = await github.rest.actions.listWorkflowRunArtifacts({ owner: context.repo.owner, repo: context.repo.repo, run_id: runId }); - if (!artifacts || artifacts.artifacts.length === 0) { - console.log('No artifacts found for this run'); - return; - } - - // Sort and categorize artifacts - const androidArtifacts = artifacts.artifacts + // Sort and categorize all collected artifacts + const androidArtifacts = allArtifacts .filter(a => a.name.includes('android')) .sort((a, b) => a.name.localeCompare(b.name)); - const iosArtifacts = artifacts.artifacts + const iosArtifacts = allArtifacts .filter(a => a.name.includes('ios')) .sort((a, b) => a.name.localeCompare(b.name)); - // Build comment body with table format - let commentBody = `## đŸ“± Build Artifacts Ready!\n\n`; - commentBody += `✅ **Workflow completed successfully** for PR #${pr.number}\n`; - commentBody += `📩 **${artifacts.artifacts.length} artifacts** generated from commit [\`${pr.head.sha.substring(0, 7)}\`](https://github.com/${context.repo.owner}/${context.repo.repo}/commit/${pr.head.sha})\n\n`; + // Build comment body with progressive status + let commentBody = `## đŸ“± Build Status for PR #${pr.number}\n\n`; + commentBody += `🔗 **Commit**: [\`${pr.head.sha.substring(0, 7)}\`](https://github.com/${context.repo.owner}/${context.repo.repo}/commit/${pr.head.sha})\n\n`; + + // Add build status table + commentBody += `### 🔧 Build Status\n\n`; + commentBody += `| Workflow | Status | Artifacts |\n`; + commentBody += `|----------|--------|-----------|\n`; + + for (const [name, status] of Object.entries(buildStatuses)) { + const emoji = status.conclusion === 'success' ? '✅' : + status.conclusion === 'failure' ? '❌' : + status.status === 'in_progress' ? '🔄' : '⏳'; + const statusText = status.conclusion || status.status || 'pending'; + const artifactCount = allArtifacts.filter(a => { + // Match artifacts to workflows based on naming patterns + if (name.includes('Android')) return a.name.includes('android'); + if (name.includes('iOS')) return a.name.includes('ios'); + return false; + }).length; + + commentBody += `| [${name}](${status.url}) | ${emoji} ${statusText} | ${artifactCount} |\n`; + } + + commentBody += `\n`; + + // Only show download table if there are artifacts + if (allArtifacts.length > 0) { + commentBody += `### 📩 Available Downloads (${allArtifacts.length} artifacts)\n\n`; - if (androidArtifacts.length === 0 && iosArtifacts.length === 0) { - commentBody += `⚠ No mobile app artifacts found in this build.\n\n`; - } else { // Create table for better organization commentBody += `| Platform | Device Type | Download Link |\n`; commentBody += `|----------|-------------|---------------|\n`; @@ -115,7 +159,7 @@ jobs: androidArtifacts.forEach(artifact => { const isTV = artifact.name.includes('tv'); const deviceType = isTV ? 'đŸ“ș Android TV' : 'đŸ“± Android Phone'; - const nightlyLink = `https://nightly.link/${context.repo.owner}/${context.repo.repo}/actions/runs/${runId}/${artifact.name}.zip`; + const nightlyLink = `https://nightly.link/${context.repo.owner}/${context.repo.repo}/actions/runs/${artifact.workflow_run.id}/${artifact.name}.zip`; commentBody += `| đŸ€– Android | ${deviceType} | [đŸ“„ Download APK](${nightlyLink}) |\n`; }); @@ -123,7 +167,7 @@ jobs: iosArtifacts.forEach(artifact => { const isTV = artifact.name.includes('tv'); const deviceType = isTV ? 'đŸ“ș Apple TV' : 'đŸ“± iPhone/iPad'; - const nightlyLink = `https://nightly.link/${context.repo.owner}/${context.repo.repo}/actions/runs/${runId}/${artifact.name}.zip`; + const nightlyLink = `https://nightly.link/${context.repo.owner}/${context.repo.repo}/actions/runs/${artifact.workflow_run.id}/${artifact.name}.zip`; commentBody += `| 🍎 iOS | ${deviceType} | [đŸ“„ Download IPA](${nightlyLink}) |\n`; }); @@ -132,6 +176,8 @@ jobs: commentBody += `- **Android APK**: Download and install directly on your device (enable "Install from unknown sources")\n`; commentBody += `- **iOS IPA**: Install using [AltStore](https://altstore.io/), [Sideloadly](https://sideloadly.io/), or Xcode\n\n`; commentBody += `> ⚠ **Note**: Artifacts expire in 7 days from build date\n\n`; + } else { + commentBody += `⏳ **No artifacts available yet** - builds are still in progress or haven't completed successfully.\n\n`; } commentBody += `*Auto-generated by [GitHub Actions](https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${runId})*`; From 0d1aeaf8aaeea68f5dbb2f8a4a32eebb374db7be Mon Sep 17 00:00:00 2001 From: Uruk Date: Mon, 29 Sep 2025 23:55:36 +0200 Subject: [PATCH 09/29] fix(ci): improve workflow run condition and artifact collection Fixes workflow trigger condition by explicitly checking for workflow_run event type to prevent unintended executions. Improves artifact collection reliability by switching to the correct API method and increasing page size to capture more artifacts from multiple builds. Removes redundant artifact fetching logic that was duplicating collection efforts. --- .github/workflows/artifact-comment.yml | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/.github/workflows/artifact-comment.yml b/.github/workflows/artifact-comment.yml index 60244409..f7d5ff04 100644 --- a/.github/workflows/artifact-comment.yml +++ b/.github/workflows/artifact-comment.yml @@ -16,7 +16,7 @@ on: jobs: comment-artifacts: - if: github.event_name == 'workflow_dispatch' || github.event.workflow_run.event == 'pull_request' + if: github.event_name == 'workflow_dispatch' || (github.event_name == 'workflow_run' && github.event.workflow_run.event == 'pull_request') runs-on: ubuntu-latest permissions: contents: read @@ -63,11 +63,11 @@ jobs: console.log(`Processing PR #${pr.number} for commit ${pr.head.sha.substring(0, 7)}`); // Get all recent workflow runs for this PR to collect artifacts from multiple builds - const { data: workflowRuns } = await github.rest.actions.listWorkflowRuns({ + const { data: workflowRuns } = await github.rest.actions.listWorkflowRunsForRepo({ owner: context.repo.owner, repo: context.repo.repo, head_sha: pr.head.sha, - per_page: 10 + per_page: 20 }); // Filter for build workflows only @@ -106,13 +106,6 @@ jobs: console.log(`Collected ${allArtifacts.length} total artifacts from all builds`); - // Get artifacts from current run if needed - const { data: artifacts } = await github.rest.actions.listWorkflowRunArtifacts({ - owner: context.repo.owner, - repo: context.repo.repo, - run_id: runId - }); - // Sort and categorize all collected artifacts const androidArtifacts = allArtifacts .filter(a => a.name.includes('android')) From 4f6863f31736f3bc5b0378d12538c101a6908963 Mon Sep 17 00:00:00 2001 From: Uruk Date: Tue, 30 Sep 2025 00:01:15 +0200 Subject: [PATCH 10/29] feat: add direct pull request trigger to artifact comment workflow Enables the workflow to run directly on pull request events (opened, synchronize, reopened) in addition to the existing workflow_run and manual dispatch triggers. Provides immediate status updates in PR checks and improves user experience by showing workflow progress directly in the pull request interface rather than only after completion of upstream workflows. --- .github/workflows/artifact-comment.yml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/artifact-comment.yml b/.github/workflows/artifact-comment.yml index f7d5ff04..4d7df677 100644 --- a/.github/workflows/artifact-comment.yml +++ b/.github/workflows/artifact-comment.yml @@ -6,6 +6,8 @@ concurrency: on: workflow_dispatch: # Allow manual testing + pull_request: # Show in PR checks and provide status updates + types: [opened, synchronize, reopened] workflow_run: workflows: - "đŸ€– Android APK Build (Phone + TV)" @@ -16,7 +18,7 @@ on: jobs: comment-artifacts: - if: github.event_name == 'workflow_dispatch' || (github.event_name == 'workflow_run' && github.event.workflow_run.event == 'pull_request') + if: github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request' || (github.event_name == 'workflow_run' && github.event.workflow_run.event == 'pull_request') runs-on: ubuntu-latest permissions: contents: read @@ -28,7 +30,7 @@ jobs: uses: actions/github-script@v8 with: script: | - // Handle both workflow_run and manual dispatch events + // Handle workflow_run, pull_request, and manual dispatch events let pr; if (context.eventName === 'workflow_run') { @@ -45,6 +47,10 @@ jobs: } pr = pullRequests[0]; + } else if (context.eventName === 'pull_request') { + // Direct PR event + pr = context.payload.pull_request; + } else if (context.eventName === 'workflow_dispatch') { // Get current PR for manual testing const prNumber = context.payload.pull_request?.number || 1101; From 5b0d2f3f7bf158c019106edcb78be97ce6b89e02 Mon Sep 17 00:00:00 2001 From: Uruk Date: Tue, 30 Sep 2025 00:03:40 +0200 Subject: [PATCH 11/29] fix(ci): correct GitHub Actions run ID reference Fixes incorrect variable reference in artifact comment generation that was using undefined `runId` instead of the proper `context.runId` from the GitHub Actions context. Also adds descriptive job name for better workflow visibility. --- .github/workflows/artifact-comment.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/artifact-comment.yml b/.github/workflows/artifact-comment.yml index 4d7df677..1e63db57 100644 --- a/.github/workflows/artifact-comment.yml +++ b/.github/workflows/artifact-comment.yml @@ -19,6 +19,7 @@ on: jobs: comment-artifacts: if: github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request' || (github.event_name == 'workflow_run' && github.event.workflow_run.event == 'pull_request') + name: 📩 Post Build Artifacts runs-on: ubuntu-latest permissions: contents: read @@ -179,7 +180,7 @@ jobs: commentBody += `⏳ **No artifacts available yet** - builds are still in progress or haven't completed successfully.\n\n`; } - commentBody += `*Auto-generated by [GitHub Actions](https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${runId})*`; + commentBody += `*Auto-generated by [GitHub Actions](https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId})*`; commentBody += `\n`; // Find existing bot comment to update From e985adf06226e06fb58a2cca6b246ed6145a63cf Mon Sep 17 00:00:00 2001 From: Uruk Date: Tue, 30 Sep 2025 00:22:51 +0200 Subject: [PATCH 12/29] feat: improve build status UI with progressive updates Restructures the artifact comment workflow to display build progress in real-time with individual platform/device status tracking. Changes the status table from workflow-based to target-based (Android Phone/TV, iOS Phone/TV) with dedicated status indicators and download links that update as builds complete. Improves user experience by showing pending builds with appropriate messaging instead of waiting for all builds to finish before displaying any information. --- .github/workflows/artifact-comment.yml | 111 +++++++++++++------------ 1 file changed, 59 insertions(+), 52 deletions(-) diff --git a/.github/workflows/artifact-comment.yml b/.github/workflows/artifact-comment.yml index 1e63db57..6a36d0f5 100644 --- a/.github/workflows/artifact-comment.yml +++ b/.github/workflows/artifact-comment.yml @@ -85,10 +85,18 @@ jobs: console.log(`Found ${buildRuns.length} build workflow runs for this commit`); - // Collect artifacts from all completed successful builds + // Collect artifacts and statuses from all builds (completed and in-progress) let allArtifacts = []; let buildStatuses = {}; + // Define all expected build targets + const expectedBuilds = { + 'Android Phone': { platform: 'Android', device: 'Phone', emoji: 'đŸ“±', pattern: 'android.*phone' }, + 'Android TV': { platform: 'Android', device: 'TV', emoji: 'đŸ“ș', pattern: 'android.*tv' }, + 'iOS Phone': { platform: 'iOS', device: 'Phone', emoji: 'đŸ“±', pattern: 'ios.*phone' }, + 'iOS TV': { platform: 'iOS', device: 'TV', emoji: 'đŸ“ș', pattern: 'ios.*tv' } + }; + for (const run of buildRuns) { buildStatuses[run.name] = { status: run.status, @@ -97,6 +105,7 @@ jobs: runId: run.id }; + // Collect artifacts from any completed successful builds if (run.conclusion === 'success') { try { const { data: artifacts } = await github.rest.actions.listWorkflowRunArtifacts({ @@ -113,71 +122,69 @@ jobs: console.log(`Collected ${allArtifacts.length} total artifacts from all builds`); - // Sort and categorize all collected artifacts - const androidArtifacts = allArtifacts - .filter(a => a.name.includes('android')) - .sort((a, b) => a.name.localeCompare(b.name)); - const iosArtifacts = allArtifacts - .filter(a => a.name.includes('ios')) - .sort((a, b) => a.name.localeCompare(b.name)); - - // Build comment body with progressive status + // Build comment body with progressive status for individual builds let commentBody = `## đŸ“± Build Status for PR #${pr.number}\n\n`; commentBody += `🔗 **Commit**: [\`${pr.head.sha.substring(0, 7)}\`](https://github.com/${context.repo.owner}/${context.repo.repo}/commit/${pr.head.sha})\n\n`; - // Add build status table - commentBody += `### 🔧 Build Status\n\n`; - commentBody += `| Workflow | Status | Artifacts |\n`; - commentBody += `|----------|--------|-----------|\n`; + // Progressive build status and downloads table + commentBody += `### ïżœ Build Artifacts\n\n`; + commentBody += `| Platform | Device | Status | Download |\n`; + commentBody += `|----------|--------|--------|---------|\n`; - for (const [name, status] of Object.entries(buildStatuses)) { - const emoji = status.conclusion === 'success' ? '✅' : - status.conclusion === 'failure' ? '❌' : - status.status === 'in_progress' ? '🔄' : '⏳'; - const statusText = status.conclusion || status.status || 'pending'; - const artifactCount = allArtifacts.filter(a => { - // Match artifacts to workflows based on naming patterns - if (name.includes('Android')) return a.name.includes('android'); - if (name.includes('iOS')) return a.name.includes('ios'); - return false; - }).length; - - commentBody += `| [${name}](${status.url}) | ${emoji} ${statusText} | ${artifactCount} |\n`; + // Process each expected build target individually + const buildTargets = [ + { name: 'Android Phone', platform: 'đŸ€–', device: 'đŸ“±', pattern: /android.*phone/i }, + { name: 'Android TV', platform: 'đŸ€–', device: 'đŸ“ș', pattern: /android.*tv/i }, + { name: 'iOS Phone', platform: '🍎', device: 'ïżœ', pattern: /ios.*phone/i }, + { name: 'iOS TV', platform: '🍎', device: 'đŸ“ș', pattern: /ios.*tv/i } + ]; + + for (const target of buildTargets) { + // Find matching workflow run + const matchingRun = buildRuns.find(run => { + return (run.name.includes('Android') && target.name.includes('Android')) || + (run.name.includes('iOS') && target.name.includes('iOS')); + }); + + // Find matching artifact + const matchingArtifact = allArtifacts.find(artifact => + target.pattern.test(artifact.name) + ); + + let status = '⏳ Pending'; + let downloadLink = '*Waiting for build...*'; + + if (matchingRun) { + if (matchingRun.conclusion === 'success' && matchingArtifact) { + status = '✅ Complete'; + const nightlyLink = `https://nightly.link/${context.repo.owner}/${context.repo.repo}/actions/runs/${matchingArtifact.workflow_run.id}/${matchingArtifact.name}.zip`; + const fileType = target.name.includes('Android') ? 'APK' : 'IPA'; + downloadLink = `[đŸ“„ Download ${fileType}](${nightlyLink})`; + } else if (matchingRun.conclusion === 'failure') { + status = `❌ [Failed](${matchingRun.html_url})`; + downloadLink = '*Build failed*'; + } else if (matchingRun.status === 'in_progress') { + status = `🔄 [Building...](${matchingRun.html_url})`; + downloadLink = '*Build in progress...*'; + } else if (matchingRun.status === 'queued') { + status = `⏳ [Queued](${matchingRun.html_url})`; + downloadLink = '*Waiting to start...*'; + } + } + + commentBody += `| ${target.platform} ${target.name.split(' ')[0]} | ${target.device} ${target.name.split(' ')[1]} | ${status} | ${downloadLink} |\n`; } commentBody += `\n`; - // Only show download table if there are artifacts + // Show installation instructions if we have any artifacts if (allArtifacts.length > 0) { - commentBody += `### 📩 Available Downloads (${allArtifacts.length} artifacts)\n\n`; - - // Create table for better organization - commentBody += `| Platform | Device Type | Download Link |\n`; - commentBody += `|----------|-------------|---------------|\n`; - - // Add Android artifacts - androidArtifacts.forEach(artifact => { - const isTV = artifact.name.includes('tv'); - const deviceType = isTV ? 'đŸ“ș Android TV' : 'đŸ“± Android Phone'; - const nightlyLink = `https://nightly.link/${context.repo.owner}/${context.repo.repo}/actions/runs/${artifact.workflow_run.id}/${artifact.name}.zip`; - commentBody += `| đŸ€– Android | ${deviceType} | [đŸ“„ Download APK](${nightlyLink}) |\n`; - }); - - // Add iOS artifacts - iosArtifacts.forEach(artifact => { - const isTV = artifact.name.includes('tv'); - const deviceType = isTV ? 'đŸ“ș Apple TV' : 'đŸ“± iPhone/iPad'; - const nightlyLink = `https://nightly.link/${context.repo.owner}/${context.repo.repo}/actions/runs/${artifact.workflow_run.id}/${artifact.name}.zip`; - commentBody += `| 🍎 iOS | ${deviceType} | [đŸ“„ Download IPA](${nightlyLink}) |\n`; - }); - - commentBody += `\n`; commentBody += `### 🔧 Installation Instructions\n\n`; commentBody += `- **Android APK**: Download and install directly on your device (enable "Install from unknown sources")\n`; commentBody += `- **iOS IPA**: Install using [AltStore](https://altstore.io/), [Sideloadly](https://sideloadly.io/), or Xcode\n\n`; commentBody += `> ⚠ **Note**: Artifacts expire in 7 days from build date\n\n`; } else { - commentBody += `⏳ **No artifacts available yet** - builds are still in progress or haven't completed successfully.\n\n`; + commentBody += `⏳ **Builds are starting up...** This comment will update automatically as each build completes.\n\n`; } commentBody += `*Auto-generated by [GitHub Actions](https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId})*`; From 44e489f40c66deac09ae9e93d5640301aff57496 Mon Sep 17 00:00:00 2001 From: Uruk Date: Tue, 30 Sep 2025 00:29:13 +0200 Subject: [PATCH 13/29] refactor: improve workflow run tracking and status display Enhances the artifact comment workflow by switching from tracking all build runs to focusing on the most recent run per workflow type (Android/iOS). Changes include: - Increases pagination limit to capture more workflow runs - Sorts runs by creation time to identify latest builds - Simplifies status tracking by workflow platform rather than individual runs - Adds detailed logging for debugging build statuses - Improves error handling for artifact collection - Fixes emoji rendering issue in status display Reduces complexity while ensuring accurate status reporting for the latest builds. --- .github/workflows/artifact-comment.yml | 112 ++++++++++++++++--------- 1 file changed, 72 insertions(+), 40 deletions(-) diff --git a/.github/workflows/artifact-comment.yml b/.github/workflows/artifact-comment.yml index 6a36d0f5..1aa981f1 100644 --- a/.github/workflows/artifact-comment.yml +++ b/.github/workflows/artifact-comment.yml @@ -74,48 +74,79 @@ jobs: owner: context.repo.owner, repo: context.repo.repo, head_sha: pr.head.sha, - per_page: 20 + per_page: 30 }); - // Filter for build workflows only - const buildRuns = workflowRuns.workflow_runs.filter(run => - run.name.includes('Android APK Build') || - run.name.includes('iOS IPA Build') - ); + // Filter for build workflows only and sort by creation time (most recent first) + const buildRuns = workflowRuns.workflow_runs + .filter(run => + run.name.includes('Android APK Build') || + run.name.includes('iOS IPA Build') + ) + .sort((a, b) => new Date(b.created_at) - new Date(a.created_at)); console.log(`Found ${buildRuns.length} build workflow runs for this commit`); - // Collect artifacts and statuses from all builds (completed and in-progress) + // Log current status of each build for debugging + buildRuns.forEach(run => { + console.log(`- ${run.name}: ${run.status} (${run.conclusion || 'no conclusion yet'}) - Created: ${run.created_at}`); + }); + + // Collect artifacts and statuses from builds - get most recent run for each workflow type let allArtifacts = []; let buildStatuses = {}; - // Define all expected build targets - const expectedBuilds = { - 'Android Phone': { platform: 'Android', device: 'Phone', emoji: 'đŸ“±', pattern: 'android.*phone' }, - 'Android TV': { platform: 'Android', device: 'TV', emoji: 'đŸ“ș', pattern: 'android.*tv' }, - 'iOS Phone': { platform: 'iOS', device: 'Phone', emoji: 'đŸ“±', pattern: 'ios.*phone' }, - 'iOS TV': { platform: 'iOS', device: 'TV', emoji: 'đŸ“ș', pattern: 'ios.*tv' } - }; + // Get the most recent run for each workflow type + const latestAndroidRun = buildRuns.find(run => run.name.includes('Android APK Build')); + const latestIOSRun = buildRuns.find(run => run.name.includes('iOS IPA Build')); - for (const run of buildRuns) { - buildStatuses[run.name] = { - status: run.status, - conclusion: run.conclusion, - url: run.html_url, - runId: run.id + // Store status for each workflow type + if (latestAndroidRun) { + buildStatuses['Android'] = { + name: latestAndroidRun.name, + status: latestAndroidRun.status, + conclusion: latestAndroidRun.conclusion, + url: latestAndroidRun.html_url, + runId: latestAndroidRun.id, + created_at: latestAndroidRun.created_at }; - // Collect artifacts from any completed successful builds - if (run.conclusion === 'success') { + // Collect artifacts if completed successfully + if (latestAndroidRun.conclusion === 'success') { try { const { data: artifacts } = await github.rest.actions.listWorkflowRunArtifacts({ owner: context.repo.owner, repo: context.repo.repo, - run_id: run.id + run_id: latestAndroidRun.id }); allArtifacts.push(...artifacts.artifacts); } catch (error) { - console.log(`Failed to get artifacts for run ${run.id}:`, error.message); + console.log(`Failed to get Android artifacts for run ${latestAndroidRun.id}:`, error.message); + } + } + } + + if (latestIOSRun) { + buildStatuses['iOS'] = { + name: latestIOSRun.name, + status: latestIOSRun.status, + conclusion: latestIOSRun.conclusion, + url: latestIOSRun.html_url, + runId: latestIOSRun.id, + created_at: latestIOSRun.created_at + }; + + // Collect artifacts if completed successfully + if (latestIOSRun.conclusion === 'success') { + try { + const { data: artifacts } = await github.rest.actions.listWorkflowRunArtifacts({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: latestIOSRun.id + }); + allArtifacts.push(...artifacts.artifacts); + } catch (error) { + console.log(`Failed to get iOS artifacts for run ${latestIOSRun.id}:`, error.message); } } } @@ -123,11 +154,11 @@ jobs: console.log(`Collected ${allArtifacts.length} total artifacts from all builds`); // Build comment body with progressive status for individual builds - let commentBody = `## đŸ“± Build Status for PR #${pr.number}\n\n`; + let commentBody = `## 🔧 Build Status for PR #${pr.number}\n\n`; commentBody += `🔗 **Commit**: [\`${pr.head.sha.substring(0, 7)}\`](https://github.com/${context.repo.owner}/${context.repo.repo}/commit/${pr.head.sha})\n\n`; // Progressive build status and downloads table - commentBody += `### ïżœ Build Artifacts\n\n`; + commentBody += `### 📩 Build Artifacts\n\n`; commentBody += `| Platform | Device | Status | Download |\n`; commentBody += `|----------|--------|--------|---------|\n`; @@ -135,16 +166,13 @@ jobs: const buildTargets = [ { name: 'Android Phone', platform: 'đŸ€–', device: 'đŸ“±', pattern: /android.*phone/i }, { name: 'Android TV', platform: 'đŸ€–', device: 'đŸ“ș', pattern: /android.*tv/i }, - { name: 'iOS Phone', platform: '🍎', device: 'ïżœ', pattern: /ios.*phone/i }, + { name: 'iOS Phone', platform: '🍎', device: 'đŸ“±', pattern: /ios.*phone/i }, { name: 'iOS TV', platform: '🍎', device: 'đŸ“ș', pattern: /ios.*tv/i } ]; for (const target of buildTargets) { - // Find matching workflow run - const matchingRun = buildRuns.find(run => { - return (run.name.includes('Android') && target.name.includes('Android')) || - (run.name.includes('iOS') && target.name.includes('iOS')); - }); + // Find matching workflow status (using our simplified structure) + const matchingStatus = target.name.includes('Android') ? buildStatuses['Android'] : buildStatuses['iOS']; // Find matching artifact const matchingArtifact = allArtifacts.find(artifact => @@ -154,21 +182,25 @@ jobs: let status = '⏳ Pending'; let downloadLink = '*Waiting for build...*'; - if (matchingRun) { - if (matchingRun.conclusion === 'success' && matchingArtifact) { + if (matchingStatus) { + if (matchingStatus.conclusion === 'success' && matchingArtifact) { status = '✅ Complete'; const nightlyLink = `https://nightly.link/${context.repo.owner}/${context.repo.repo}/actions/runs/${matchingArtifact.workflow_run.id}/${matchingArtifact.name}.zip`; const fileType = target.name.includes('Android') ? 'APK' : 'IPA'; downloadLink = `[đŸ“„ Download ${fileType}](${nightlyLink})`; - } else if (matchingRun.conclusion === 'failure') { - status = `❌ [Failed](${matchingRun.html_url})`; + } else if (matchingStatus.conclusion === 'failure') { + status = `❌ [Failed](${matchingStatus.url})`; downloadLink = '*Build failed*'; - } else if (matchingRun.status === 'in_progress') { - status = `🔄 [Building...](${matchingRun.html_url})`; + } else if (matchingStatus.status === 'in_progress') { + status = `🔄 [Building...](${matchingStatus.url})`; downloadLink = '*Build in progress...*'; - } else if (matchingRun.status === 'queued') { - status = `⏳ [Queued](${matchingRun.html_url})`; + } else if (matchingStatus.status === 'queued') { + status = `⏳ [Queued](${matchingStatus.url})`; downloadLink = '*Waiting to start...*'; + } else { + // Show any other status with timestamp for debugging + status = `🔄 [${matchingStatus.status}](${matchingStatus.url})`; + downloadLink = `*Status: ${matchingStatus.status}*`; } } From 2b761f15c82a0ae522bb3e3944f896aef4bf3846 Mon Sep 17 00:00:00 2001 From: Uruk Date: Tue, 30 Sep 2025 00:51:02 +0200 Subject: [PATCH 14/29] feat!: replace workflow_run with repository_dispatch for real-time build status updates Replaces the workflow_run trigger mechanism with repository_dispatch events to enable real-time build status communication between build workflows and the artifact comment system. Build workflows now actively notify the comment workflow when builds start, complete, or fail, providing immediate status updates rather than polling for completed workflows. Adds real-time payload processing to display current build status and target information in PR comments, improving visibility into ongoing build processes. BREAKING CHANGE: Changes the trigger mechanism from workflow_run to repository_dispatch, requiring build workflows to explicitly send status notifications. --- .github/workflows/artifact-comment.yml | 110 ++++++++++++++++++------- .github/workflows/build-android.yml | 61 ++++++++++++++ .github/workflows/build-ios.yml | 67 ++++++++++++++- 3 files changed, 207 insertions(+), 31 deletions(-) diff --git a/.github/workflows/artifact-comment.yml b/.github/workflows/artifact-comment.yml index 1aa981f1..0bfb99af 100644 --- a/.github/workflows/artifact-comment.yml +++ b/.github/workflows/artifact-comment.yml @@ -1,56 +1,61 @@ name: 📝 Artifact Comment on PR concurrency: - group: artifact-comment-${{ github.event.workflow_run.head_sha || github.sha }} + group: artifact-comment-${{ github.sha }} cancel-in-progress: true on: workflow_dispatch: # Allow manual testing pull_request: # Show in PR checks and provide status updates types: [opened, synchronize, reopened] - workflow_run: - workflows: - - "đŸ€– Android APK Build (Phone + TV)" - - "đŸ€– iOS IPA Build (Phone + TV)" + repository_dispatch: # Triggered by build workflows when they start/complete types: - - completed - - requested # Trigger when build starts + - build-started + - build-completed + - build-failed jobs: comment-artifacts: - if: github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request' || (github.event_name == 'workflow_run' && github.event.workflow_run.event == 'pull_request') + if: github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request' || github.event_name == 'repository_dispatch' name: 📩 Post Build Artifacts runs-on: ubuntu-latest permissions: contents: read pull-requests: write actions: read + repository-projects: read steps: - name: 🔍 Get PR and Artifacts uses: actions/github-script@v8 with: script: | - // Handle workflow_run, pull_request, and manual dispatch events + // Handle repository_dispatch, pull_request, and manual dispatch events let pr; + let targetCommitSha; - if (context.eventName === 'workflow_run') { - // Find PR associated with this commit - const { data: pullRequests } = await github.rest.repos.listPullRequestsAssociatedWithCommit({ - owner: context.repo.owner, - repo: context.repo.repo, - commit_sha: github.event.workflow_run.head_sha - }); + if (context.eventName === 'repository_dispatch') { + // Triggered by build workflows - get PR info from payload + const payload = context.payload.client_payload; + console.log('Repository dispatch payload:', JSON.stringify(payload, null, 2)); - if (pullRequests.length === 0) { - console.log('No pull request found for commit:', github.event.workflow_run.head_sha); + if (!payload || !payload.pr_number) { + console.log('No PR information in repository_dispatch payload'); return; } - pr = pullRequests[0]; + + const { data: prData } = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: payload.pr_number + }); + pr = prData; + targetCommitSha = payload.commit_sha || pr.head.sha; } else if (context.eventName === 'pull_request') { // Direct PR event pr = context.payload.pull_request; + targetCommitSha = pr.head.sha; } else if (context.eventName === 'workflow_dispatch') { // Get current PR for manual testing @@ -61,19 +66,20 @@ jobs: pull_number: prNumber }); pr = prData; + targetCommitSha = pr.head.sha; } else { console.log('Unsupported event type:', context.eventName); return; } - console.log(`Processing PR #${pr.number} for commit ${pr.head.sha.substring(0, 7)}`); + console.log(`Processing PR #${pr.number} for commit ${targetCommitSha.substring(0, 7)}`); // Get all recent workflow runs for this PR to collect artifacts from multiple builds const { data: workflowRuns } = await github.rest.actions.listWorkflowRunsForRepo({ owner: context.repo.owner, repo: context.repo.repo, - head_sha: pr.head.sha, + head_sha: targetCommitSha, per_page: 30 }); @@ -151,11 +157,50 @@ jobs: } } + // Override with real-time data from repository_dispatch if available + if (context.eventName === 'repository_dispatch') { + const payload = context.payload.client_payload; + const workflowType = payload.workflow_name.includes('Android') ? 'Android' : 'iOS'; + + if (buildStatuses[workflowType]) { + // Update the existing status with real-time data + buildStatuses[workflowType].status = payload.status === 'in_progress' ? 'in_progress' : + payload.status === 'success' ? 'completed' : + payload.status === 'failure' ? 'completed' : + buildStatuses[workflowType].status; + buildStatuses[workflowType].conclusion = payload.status === 'success' ? 'success' : + payload.status === 'failure' ? 'failure' : + buildStatuses[workflowType].conclusion; + buildStatuses[workflowType].url = payload.run_url; + buildStatuses[workflowType].target = payload.target; + } else { + // Create new status entry for real-time updates + buildStatuses[workflowType] = { + name: payload.workflow_name, + status: payload.status === 'in_progress' ? 'in_progress' : + payload.status === 'success' ? 'completed' : + payload.status === 'failure' ? 'completed' : 'queued', + conclusion: payload.status === 'success' ? 'success' : + payload.status === 'failure' ? 'failure' : null, + url: payload.run_url, + runId: payload.run_id, + target: payload.target, + created_at: new Date().toISOString() + }; + } + } + console.log(`Collected ${allArtifacts.length} total artifacts from all builds`); // Build comment body with progressive status for individual builds let commentBody = `## 🔧 Build Status for PR #${pr.number}\n\n`; - commentBody += `🔗 **Commit**: [\`${pr.head.sha.substring(0, 7)}\`](https://github.com/${context.repo.owner}/${context.repo.repo}/commit/${pr.head.sha})\n\n`; + commentBody += `🔗 **Commit**: [\`${targetCommitSha.substring(0, 7)}\`](https://github.com/${context.repo.owner}/${context.repo.repo}/commit/${targetCommitSha})\n\n`; + + // Show event context for debugging (only for repository_dispatch) + if (context.eventName === 'repository_dispatch') { + const payload = context.payload.client_payload; + commentBody += `🔔 **Real-time Update**: ${payload.workflow_name} (${payload.target}) - ${payload.status}\n\n`; + } // Progressive build status and downloads table commentBody += `### 📩 Build Artifacts\n\n`; @@ -164,15 +209,24 @@ jobs: // Process each expected build target individually const buildTargets = [ - { name: 'Android Phone', platform: 'đŸ€–', device: 'đŸ“±', pattern: /android.*phone/i }, - { name: 'Android TV', platform: 'đŸ€–', device: 'đŸ“ș', pattern: /android.*tv/i }, - { name: 'iOS Phone', platform: '🍎', device: 'đŸ“±', pattern: /ios.*phone/i }, - { name: 'iOS TV', platform: '🍎', device: 'đŸ“ș', pattern: /ios.*tv/i } + { name: 'Android Phone', platform: 'đŸ€–', device: 'đŸ“±', workflowType: 'Android', target: 'phone' }, + { name: 'Android TV', platform: 'đŸ€–', device: 'đŸ“ș', workflowType: 'Android', target: 'tv' }, + { name: 'iOS Phone', platform: '🍎', device: 'đŸ“±', workflowType: 'iOS', target: 'phone' }, + { name: 'iOS TV', platform: '🍎', device: 'đŸ“ș', workflowType: 'iOS', target: 'tv' } ]; for (const target of buildTargets) { - // Find matching workflow status (using our simplified structure) - const matchingStatus = target.name.includes('Android') ? buildStatuses['Android'] : buildStatuses['iOS']; + // Find matching workflow status + let matchingStatus = buildStatuses[target.workflowType]; + + // For repository_dispatch events, check if this specific target matches + if (context.eventName === 'repository_dispatch' && matchingStatus) { + const payload = context.payload.client_payload; + if (payload.target !== target.target) { + // This update is for a different target, show default status + matchingStatus = null; + } + } // Find matching artifact const matchingArtifact = allArtifacts.find(artifact => diff --git a/.github/workflows/build-android.yml b/.github/workflows/build-android.yml index bc9b9ea1..d273f616 100644 --- a/.github/workflows/build-android.yml +++ b/.github/workflows/build-android.yml @@ -18,6 +18,7 @@ jobs: name: đŸ—ïž Build Android APK permissions: contents: read + repository-projects: write strategy: fail-fast: false @@ -25,6 +26,26 @@ jobs: target: [phone, tv] steps: + - name: 📱 Notify artifact comment workflow (started) + if: github.event_name == 'pull_request' + uses: actions/github-script@v8 + with: + script: | + await github.rest.repos.createDispatchEvent({ + owner: context.repo.owner, + repo: context.repo.repo, + event_type: 'build-started', + client_payload: { + workflow_name: 'Android APK Build', + target: '${{ matrix.target }}', + status: 'in_progress', + pr_number: ${{ github.event.pull_request.number }}, + commit_sha: '${{ github.event.pull_request.head.sha }}', + run_id: ${{ github.run_id }}, + run_url: '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}' + } + }); + - name: đŸ“„ Checkout code uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: @@ -91,3 +112,43 @@ jobs: path: | android/app/build/outputs/apk/release/*.apk retention-days: 7 + + - name: 🔔 Notify artifact comment workflow (success) + if: success() && github.event_name == 'pull_request' + uses: actions/github-script@v8 + with: + script: | + await github.rest.repos.createDispatchEvent({ + owner: context.repo.owner, + repo: context.repo.repo, + event_type: 'build-completed', + client_payload: { + workflow_name: 'Android APK Build', + target: '${{ matrix.target }}', + status: 'success', + pr_number: ${{ github.event.pull_request.number }}, + commit_sha: '${{ github.event.pull_request.head.sha }}', + run_id: ${{ github.run_id }}, + run_url: '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}' + } + }); + + - name: 🔔 Notify artifact comment workflow (failure) + if: failure() && github.event_name == 'pull_request' + uses: actions/github-script@v8 + with: + script: | + await github.rest.repos.createDispatchEvent({ + owner: context.repo.owner, + repo: context.repo.repo, + event_type: 'build-failed', + client_payload: { + workflow_name: 'Android APK Build', + target: '${{ matrix.target }}', + status: 'failure', + pr_number: ${{ github.event.pull_request.number }}, + commit_sha: '${{ github.event.pull_request.head.sha }}', + run_id: ${{ github.run_id }}, + run_url: '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}' + } + }); diff --git a/.github/workflows/build-ios.yml b/.github/workflows/build-ios.yml index 98b587b3..27c5be6c 100644 --- a/.github/workflows/build-ios.yml +++ b/.github/workflows/build-ios.yml @@ -9,11 +9,11 @@ on: pull_request: branches: [develop, master] paths-ignore: - - '*.md' + - "*.md" push: branches: [develop, master] paths-ignore: - - '*.md' + - "*.md" jobs: build-ios: @@ -22,14 +22,35 @@ jobs: name: đŸ—ïž Build iOS IPA permissions: contents: read + repository-projects: write strategy: fail-fast: false matrix: target: [phone] -# target: [phone, tv] + # target: [phone, tv] steps: + - name: 📱 Notify artifact comment workflow (started) + if: github.event_name == 'pull_request' + uses: actions/github-script@v8 + with: + script: | + await github.rest.repos.createDispatchEvent({ + owner: context.repo.owner, + repo: context.repo.repo, + event_type: 'build-started', + client_payload: { + workflow_name: 'iOS IPA Build', + target: '${{ matrix.target }}', + status: 'in_progress', + pr_number: ${{ github.event.pull_request.number }}, + commit_sha: '${{ github.event.pull_request.head.sha }}', + run_id: ${{ github.run_id }}, + run_url: '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}' + } + }); + - name: đŸ“„ Checkout code uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: @@ -93,3 +114,43 @@ jobs: name: streamyfin-ios-${{ matrix.target }}-ipa-${{ env.DATE_TAG }} path: build-*.ipa retention-days: 7 + + - name: 🔔 Notify artifact comment workflow (success) + if: success() && github.event_name == 'pull_request' + uses: actions/github-script@v8 + with: + script: | + await github.rest.repos.createDispatchEvent({ + owner: context.repo.owner, + repo: context.repo.repo, + event_type: 'build-completed', + client_payload: { + workflow_name: 'iOS IPA Build', + target: '${{ matrix.target }}', + status: 'success', + pr_number: ${{ github.event.pull_request.number }}, + commit_sha: '${{ github.event.pull_request.head.sha }}', + run_id: ${{ github.run_id }}, + run_url: '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}' + } + }); + + - name: 🔔 Notify artifact comment workflow (failure) + if: failure() && github.event_name == 'pull_request' + uses: actions/github-script@v8 + with: + script: | + await github.rest.repos.createDispatchEvent({ + owner: context.repo.owner, + repo: context.repo.repo, + event_type: 'build-failed', + client_payload: { + workflow_name: 'iOS IPA Build', + target: '${{ matrix.target }}', + status: 'failure', + pr_number: ${{ github.event.pull_request.number }}, + commit_sha: '${{ github.event.pull_request.head.sha }}', + run_id: ${{ github.run_id }}, + run_url: '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}' + } + }); From 788f420ce58997567cb54422f78a094937f25ec6 Mon Sep 17 00:00:00 2001 From: Uruk Date: Tue, 30 Sep 2025 00:58:25 +0200 Subject: [PATCH 15/29] refactor: migrate artifact comment system from repository_dispatch to workflow_run Replaces the repository_dispatch event system with workflow_run triggers to improve reliability and reduce complexity. The new approach automatically detects PR associations through commit SHA lookups rather than requiring manual payload construction. Removes redundant notification steps from build workflows and simplifies the concurrency group logic. Enhances manual testing support with improved PR discovery fallbacks. --- .github/workflows/artifact-comment.yml | 142 +++++++++++-------------- .github/workflows/build-android.yml | 61 ----------- .github/workflows/build-ios.yml | 61 ----------- 3 files changed, 61 insertions(+), 203 deletions(-) diff --git a/.github/workflows/artifact-comment.yml b/.github/workflows/artifact-comment.yml index 0bfb99af..c4773b62 100644 --- a/.github/workflows/artifact-comment.yml +++ b/.github/workflows/artifact-comment.yml @@ -1,29 +1,29 @@ name: 📝 Artifact Comment on PR concurrency: - group: artifact-comment-${{ github.sha }} + group: artifact-comment-${{ github.event.workflow_run.head_sha || github.sha }} cancel-in-progress: true on: workflow_dispatch: # Allow manual testing pull_request: # Show in PR checks and provide status updates types: [opened, synchronize, reopened] - repository_dispatch: # Triggered by build workflows when they start/complete + workflow_run: # Triggered when build workflows complete + workflows: + - "đŸ€– Android APK Build (Phone + TV)" + - "đŸ€– iOS IPA Build (Phone + TV)" types: - - build-started - - build-completed - - build-failed + - completed jobs: comment-artifacts: - if: github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request' || github.event_name == 'repository_dispatch' + if: github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request' || (github.event_name == 'workflow_run' && github.event.workflow_run.event == 'pull_request') name: 📩 Post Build Artifacts runs-on: ubuntu-latest permissions: contents: read pull-requests: write actions: read - repository-projects: read steps: - name: 🔍 Get PR and Artifacts @@ -34,23 +34,23 @@ jobs: let pr; let targetCommitSha; - if (context.eventName === 'repository_dispatch') { - // Triggered by build workflows - get PR info from payload - const payload = context.payload.client_payload; - console.log('Repository dispatch payload:', JSON.stringify(payload, null, 2)); + if (context.eventName === 'workflow_run') { + // Find PR associated with this workflow run commit + console.log('Workflow run event:', context.payload.workflow_run.name); - if (!payload || !payload.pr_number) { - console.log('No PR information in repository_dispatch payload'); + const { data: pullRequests } = await github.rest.repos.listPullRequestsAssociatedWithCommit({ + owner: context.repo.owner, + repo: context.repo.repo, + commit_sha: context.payload.workflow_run.head_sha + }); + + if (pullRequests.length === 0) { + console.log('No pull request found for commit:', context.payload.workflow_run.head_sha); return; } - const { data: prData } = await github.rest.pulls.get({ - owner: context.repo.owner, - repo: context.repo.repo, - pull_number: payload.pr_number - }); - pr = prData; - targetCommitSha = payload.commit_sha || pr.head.sha; + pr = pullRequests[0]; + targetCommitSha = context.payload.workflow_run.head_sha; } else if (context.eventName === 'pull_request') { // Direct PR event @@ -58,15 +58,45 @@ jobs: targetCommitSha = pr.head.sha; } else if (context.eventName === 'workflow_dispatch') { - // Get current PR for manual testing - const prNumber = context.payload.pull_request?.number || 1101; - const { data: prData } = await github.rest.pulls.get({ - owner: context.repo.owner, - repo: context.repo.repo, - pull_number: prNumber - }); - pr = prData; - targetCommitSha = pr.head.sha; + // For manual testing, try to find PR for current branch/commit + console.log('Manual workflow dispatch triggered'); + + // First, try to find PRs associated with current commit + try { + const { data: pullRequests } = await github.rest.repos.listPullRequestsAssociatedWithCommit({ + owner: context.repo.owner, + repo: context.repo.repo, + commit_sha: context.sha + }); + + if (pullRequests.length > 0) { + pr = pullRequests[0]; + targetCommitSha = pr.head.sha; + console.log(`Found PR #${pr.number} for commit ${context.sha.substring(0, 7)}`); + } else { + // Fallback: get latest open PR + const { data: openPRs } = await github.rest.pulls.list({ + owner: context.repo.owner, + repo: context.repo.repo, + state: 'open', + sort: 'updated', + direction: 'desc', + per_page: 1 + }); + + if (openPRs.length > 0) { + pr = openPRs[0]; + targetCommitSha = pr.head.sha; + console.log(`Using latest open PR #${pr.number} for manual testing`); + } else { + console.log('No open PRs found for manual testing'); + return; + } + } + } catch (error) { + console.log('Error finding PR for manual testing:', error.message); + return; + } } else { console.log('Unsupported event type:', context.eventName); @@ -157,52 +187,11 @@ jobs: } } - // Override with real-time data from repository_dispatch if available - if (context.eventName === 'repository_dispatch') { - const payload = context.payload.client_payload; - const workflowType = payload.workflow_name.includes('Android') ? 'Android' : 'iOS'; - - if (buildStatuses[workflowType]) { - // Update the existing status with real-time data - buildStatuses[workflowType].status = payload.status === 'in_progress' ? 'in_progress' : - payload.status === 'success' ? 'completed' : - payload.status === 'failure' ? 'completed' : - buildStatuses[workflowType].status; - buildStatuses[workflowType].conclusion = payload.status === 'success' ? 'success' : - payload.status === 'failure' ? 'failure' : - buildStatuses[workflowType].conclusion; - buildStatuses[workflowType].url = payload.run_url; - buildStatuses[workflowType].target = payload.target; - } else { - // Create new status entry for real-time updates - buildStatuses[workflowType] = { - name: payload.workflow_name, - status: payload.status === 'in_progress' ? 'in_progress' : - payload.status === 'success' ? 'completed' : - payload.status === 'failure' ? 'completed' : 'queued', - conclusion: payload.status === 'success' ? 'success' : - payload.status === 'failure' ? 'failure' : null, - url: payload.run_url, - runId: payload.run_id, - target: payload.target, - created_at: new Date().toISOString() - }; - } - } - console.log(`Collected ${allArtifacts.length} total artifacts from all builds`); // Build comment body with progressive status for individual builds let commentBody = `## 🔧 Build Status for PR #${pr.number}\n\n`; - commentBody += `🔗 **Commit**: [\`${targetCommitSha.substring(0, 7)}\`](https://github.com/${context.repo.owner}/${context.repo.repo}/commit/${targetCommitSha})\n\n`; - - // Show event context for debugging (only for repository_dispatch) - if (context.eventName === 'repository_dispatch') { - const payload = context.payload.client_payload; - commentBody += `🔔 **Real-time Update**: ${payload.workflow_name} (${payload.target}) - ${payload.status}\n\n`; - } - - // Progressive build status and downloads table + commentBody += `🔗 **Commit**: [\`${targetCommitSha.substring(0, 7)}\`](https://github.com/${context.repo.owner}/${context.repo.repo}/commit/${targetCommitSha})\n\n`; // Progressive build status and downloads table commentBody += `### 📩 Build Artifacts\n\n`; commentBody += `| Platform | Device | Status | Download |\n`; commentBody += `|----------|--------|--------|---------|\n`; @@ -217,16 +206,7 @@ jobs: for (const target of buildTargets) { // Find matching workflow status - let matchingStatus = buildStatuses[target.workflowType]; - - // For repository_dispatch events, check if this specific target matches - if (context.eventName === 'repository_dispatch' && matchingStatus) { - const payload = context.payload.client_payload; - if (payload.target !== target.target) { - // This update is for a different target, show default status - matchingStatus = null; - } - } + const matchingStatus = buildStatuses[target.workflowType]; // Find matching artifact const matchingArtifact = allArtifacts.find(artifact => diff --git a/.github/workflows/build-android.yml b/.github/workflows/build-android.yml index d273f616..bc9b9ea1 100644 --- a/.github/workflows/build-android.yml +++ b/.github/workflows/build-android.yml @@ -18,7 +18,6 @@ jobs: name: đŸ—ïž Build Android APK permissions: contents: read - repository-projects: write strategy: fail-fast: false @@ -26,26 +25,6 @@ jobs: target: [phone, tv] steps: - - name: 📱 Notify artifact comment workflow (started) - if: github.event_name == 'pull_request' - uses: actions/github-script@v8 - with: - script: | - await github.rest.repos.createDispatchEvent({ - owner: context.repo.owner, - repo: context.repo.repo, - event_type: 'build-started', - client_payload: { - workflow_name: 'Android APK Build', - target: '${{ matrix.target }}', - status: 'in_progress', - pr_number: ${{ github.event.pull_request.number }}, - commit_sha: '${{ github.event.pull_request.head.sha }}', - run_id: ${{ github.run_id }}, - run_url: '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}' - } - }); - - name: đŸ“„ Checkout code uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: @@ -112,43 +91,3 @@ jobs: path: | android/app/build/outputs/apk/release/*.apk retention-days: 7 - - - name: 🔔 Notify artifact comment workflow (success) - if: success() && github.event_name == 'pull_request' - uses: actions/github-script@v8 - with: - script: | - await github.rest.repos.createDispatchEvent({ - owner: context.repo.owner, - repo: context.repo.repo, - event_type: 'build-completed', - client_payload: { - workflow_name: 'Android APK Build', - target: '${{ matrix.target }}', - status: 'success', - pr_number: ${{ github.event.pull_request.number }}, - commit_sha: '${{ github.event.pull_request.head.sha }}', - run_id: ${{ github.run_id }}, - run_url: '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}' - } - }); - - - name: 🔔 Notify artifact comment workflow (failure) - if: failure() && github.event_name == 'pull_request' - uses: actions/github-script@v8 - with: - script: | - await github.rest.repos.createDispatchEvent({ - owner: context.repo.owner, - repo: context.repo.repo, - event_type: 'build-failed', - client_payload: { - workflow_name: 'Android APK Build', - target: '${{ matrix.target }}', - status: 'failure', - pr_number: ${{ github.event.pull_request.number }}, - commit_sha: '${{ github.event.pull_request.head.sha }}', - run_id: ${{ github.run_id }}, - run_url: '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}' - } - }); diff --git a/.github/workflows/build-ios.yml b/.github/workflows/build-ios.yml index 27c5be6c..76e4a4d2 100644 --- a/.github/workflows/build-ios.yml +++ b/.github/workflows/build-ios.yml @@ -22,7 +22,6 @@ jobs: name: đŸ—ïž Build iOS IPA permissions: contents: read - repository-projects: write strategy: fail-fast: false @@ -31,26 +30,6 @@ jobs: # target: [phone, tv] steps: - - name: 📱 Notify artifact comment workflow (started) - if: github.event_name == 'pull_request' - uses: actions/github-script@v8 - with: - script: | - await github.rest.repos.createDispatchEvent({ - owner: context.repo.owner, - repo: context.repo.repo, - event_type: 'build-started', - client_payload: { - workflow_name: 'iOS IPA Build', - target: '${{ matrix.target }}', - status: 'in_progress', - pr_number: ${{ github.event.pull_request.number }}, - commit_sha: '${{ github.event.pull_request.head.sha }}', - run_id: ${{ github.run_id }}, - run_url: '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}' - } - }); - - name: đŸ“„ Checkout code uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: @@ -114,43 +93,3 @@ jobs: name: streamyfin-ios-${{ matrix.target }}-ipa-${{ env.DATE_TAG }} path: build-*.ipa retention-days: 7 - - - name: 🔔 Notify artifact comment workflow (success) - if: success() && github.event_name == 'pull_request' - uses: actions/github-script@v8 - with: - script: | - await github.rest.repos.createDispatchEvent({ - owner: context.repo.owner, - repo: context.repo.repo, - event_type: 'build-completed', - client_payload: { - workflow_name: 'iOS IPA Build', - target: '${{ matrix.target }}', - status: 'success', - pr_number: ${{ github.event.pull_request.number }}, - commit_sha: '${{ github.event.pull_request.head.sha }}', - run_id: ${{ github.run_id }}, - run_url: '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}' - } - }); - - - name: 🔔 Notify artifact comment workflow (failure) - if: failure() && github.event_name == 'pull_request' - uses: actions/github-script@v8 - with: - script: | - await github.rest.repos.createDispatchEvent({ - owner: context.repo.owner, - repo: context.repo.repo, - event_type: 'build-failed', - client_payload: { - workflow_name: 'iOS IPA Build', - target: '${{ matrix.target }}', - status: 'failure', - pr_number: ${{ github.event.pull_request.number }}, - commit_sha: '${{ github.event.pull_request.head.sha }}', - run_id: ${{ github.run_id }}, - run_url: '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}' - } - }); From fb0a70690eed20d82950d50097b214b9269b2d26 Mon Sep 17 00:00:00 2001 From: Uruk Date: Tue, 30 Sep 2025 01:20:11 +0200 Subject: [PATCH 16/29] refactor: consolidate build workflows into unified app builder Merges separate Android and iOS build workflows into a single "Build Apps" workflow to reduce duplication and simplify maintenance. Updates the artifact comment workflow to handle both the new unified workflow and legacy separate workflows for backward compatibility during transition. Removes redundant workflow files while preserving all existing functionality. --- .github/workflows/artifact-comment.yml | 54 ++--- .github/workflows/build-android.yml | 93 -------- .github/workflows/build-apps.yml | 280 +++++++++++++++++++++++++ .github/workflows/build-ios.yml | 95 --------- 4 files changed, 309 insertions(+), 213 deletions(-) delete mode 100644 .github/workflows/build-android.yml create mode 100644 .github/workflows/build-apps.yml delete mode 100644 .github/workflows/build-ios.yml diff --git a/.github/workflows/artifact-comment.yml b/.github/workflows/artifact-comment.yml index c4773b62..08710fe8 100644 --- a/.github/workflows/artifact-comment.yml +++ b/.github/workflows/artifact-comment.yml @@ -10,8 +10,7 @@ on: types: [opened, synchronize, reopened] workflow_run: # Triggered when build workflows complete workflows: - - "đŸ€– Android APK Build (Phone + TV)" - - "đŸ€– iOS IPA Build (Phone + TV)" + - "đŸ—ïž Build Apps" types: - completed @@ -116,6 +115,7 @@ jobs: // Filter for build workflows only and sort by creation time (most recent first) const buildRuns = workflowRuns.workflow_runs .filter(run => + run.name.includes('Build Apps') || run.name.includes('Android APK Build') || run.name.includes('iOS IPA Build') ) @@ -132,57 +132,61 @@ jobs: let allArtifacts = []; let buildStatuses = {}; - // Get the most recent run for each workflow type + // Get the most recent run for the unified apps workflow + const latestAppsRun = buildRuns.find(run => run.name.includes('Build Apps')); const latestAndroidRun = buildRuns.find(run => run.name.includes('Android APK Build')); const latestIOSRun = buildRuns.find(run => run.name.includes('iOS IPA Build')); - // Store status for each workflow type - if (latestAndroidRun) { + // Store status for unified workflow (preferred) or fallback to separate workflows + const appsWorkflowRun = latestAppsRun || latestAndroidRun; + if (appsWorkflowRun) { buildStatuses['Android'] = { - name: latestAndroidRun.name, - status: latestAndroidRun.status, - conclusion: latestAndroidRun.conclusion, - url: latestAndroidRun.html_url, - runId: latestAndroidRun.id, - created_at: latestAndroidRun.created_at + name: appsWorkflowRun.name, + status: appsWorkflowRun.status, + conclusion: appsWorkflowRun.conclusion, + url: appsWorkflowRun.html_url, + runId: appsWorkflowRun.id, + created_at: appsWorkflowRun.created_at }; // Collect artifacts if completed successfully - if (latestAndroidRun.conclusion === 'success') { + if (appsWorkflowRun.conclusion === 'success') { try { const { data: artifacts } = await github.rest.actions.listWorkflowRunArtifacts({ owner: context.repo.owner, repo: context.repo.repo, - run_id: latestAndroidRun.id + run_id: appsWorkflowRun.id }); allArtifacts.push(...artifacts.artifacts); } catch (error) { - console.log(`Failed to get Android artifacts for run ${latestAndroidRun.id}:`, error.message); + console.log(`Failed to get apps artifacts for run ${appsWorkflowRun.id}:`, error.message); } } } - if (latestIOSRun) { + // For iOS, use the same workflow run (since it's all in one now) or fallback to separate + const iosWorkflowRun = latestAppsRun || latestIOSRun; + if (iosWorkflowRun) { buildStatuses['iOS'] = { - name: latestIOSRun.name, - status: latestIOSRun.status, - conclusion: latestIOSRun.conclusion, - url: latestIOSRun.html_url, - runId: latestIOSRun.id, - created_at: latestIOSRun.created_at + name: iosWorkflowRun.name, + status: iosWorkflowRun.status, + conclusion: iosWorkflowRun.conclusion, + url: iosWorkflowRun.html_url, + runId: iosWorkflowRun.id, + created_at: iosWorkflowRun.created_at }; - // Collect artifacts if completed successfully - if (latestIOSRun.conclusion === 'success') { + // Only collect artifacts if not already collected from apps workflow + if (!latestAppsRun && iosWorkflowRun.conclusion === 'success') { try { const { data: artifacts } = await github.rest.actions.listWorkflowRunArtifacts({ owner: context.repo.owner, repo: context.repo.repo, - run_id: latestIOSRun.id + run_id: iosWorkflowRun.id }); allArtifacts.push(...artifacts.artifacts); } catch (error) { - console.log(`Failed to get iOS artifacts for run ${latestIOSRun.id}:`, error.message); + console.log(`Failed to get iOS artifacts for run ${iosWorkflowRun.id}:`, error.message); } } } diff --git a/.github/workflows/build-android.yml b/.github/workflows/build-android.yml deleted file mode 100644 index bc9b9ea1..00000000 --- a/.github/workflows/build-android.yml +++ /dev/null @@ -1,93 +0,0 @@ -name: đŸ€– Android APK Build (Phone + TV) - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -on: - workflow_dispatch: - pull_request: - branches: [develop, master] - push: - branches: [develop, master] - -jobs: - build-android: - if: (!contains(github.event.head_commit.message, '[skip ci]')) - runs-on: ubuntu-24.04 - name: đŸ—ïž Build Android APK - permissions: - contents: read - - strategy: - fail-fast: false - matrix: - target: [phone, tv] - - steps: - - name: đŸ“„ Checkout code - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - with: - ref: ${{ github.event.pull_request.head.sha || github.sha }} - fetch-depth: 0 - submodules: recursive - show-progress: false - - - name: 🍞 Setup Bun - uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76 # v2.0.2 - with: - bun-version: latest - - - name: đŸ’Ÿ Cache Bun dependencies - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 - with: - path: ~/.bun/install/cache - key: ${{ runner.os }}-${{ runner.arch }}-bun-develop-${{ hashFiles('bun.lock') }} - restore-keys: | - ${{ runner.os }}-${{ runner.arch }}-bun-develop - ${{ runner.os }}-bun-develop - - - name: 📩 Install dependencies and reload submodules - run: | - bun install --frozen-lockfile - bun run submodule-reload - - - name: đŸ’Ÿ Cache Gradle global - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 - with: - path: | - ~/.gradle/caches - ~/.gradle/wrapper - key: ${{ runner.os }}-gradle-develop-${{ hashFiles('android/**/build.gradle', 'android/gradle/wrapper/gradle-wrapper.properties') }} - restore-keys: ${{ runner.os }}-gradle-develop - - - name: đŸ› ïž Generate project files - run: | - if [ "${{ matrix.target }}" = "tv" ]; then - bun run prebuild:tv - else - bun run prebuild - fi - - - name: đŸ’Ÿ Cache project Gradle (.gradle) - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 - with: - path: android/.gradle - key: ${{ runner.os }}-android-gradle-develop-${{ hashFiles('android/**/build.gradle', 'android/gradle/wrapper/gradle-wrapper.properties') }} - restore-keys: ${{ runner.os }}-android-gradle-develop - - - name: 🚀 Build APK - env: - EXPO_TV: ${{ matrix.target == 'tv' && 1 || 0 }} - run: bun run build:android:local - - - name: 📅 Set date tag - run: echo "DATE_TAG=$(date +%d-%m-%Y_%H-%M-%S)" >> $GITHUB_ENV - - - name: đŸ“€ Upload APK artifact - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 - with: - name: streamyfin-android-${{ matrix.target }}-apk-${{ env.DATE_TAG }} - path: | - android/app/build/outputs/apk/release/*.apk - retention-days: 7 diff --git a/.github/workflows/build-apps.yml b/.github/workflows/build-apps.yml new file mode 100644 index 00000000..ad770414 --- /dev/null +++ b/.github/workflows/build-apps.yml @@ -0,0 +1,280 @@ +name: đŸ—ïž Build Apps + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +on: + workflow_dispatch: + pull_request: + branches: [develop, master] + push: + branches: [develop, master] + +jobs: + build-android-phone: + if: (!contains(github.event.head_commit.message, '[skip ci]')) + runs-on: ubuntu-24.04 + name: đŸ€– Build Android APK (Phone) + permissions: + contents: read + + steps: + - name: đŸ“„ Checkout code + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + ref: ${{ github.event.pull_request.head.sha || github.sha }} + fetch-depth: 0 + submodules: recursive + show-progress: false + + - name: 🍞 Setup Bun + uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76 # v2.0.2 + with: + bun-version: latest + + - name: đŸ’Ÿ Cache Bun dependencies + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + with: + path: ~/.bun/install/cache + key: ${{ runner.os }}-${{ runner.arch }}-bun-develop-${{ hashFiles('bun.lock') }} + restore-keys: | + ${{ runner.os }}-${{ runner.arch }}-bun-develop + ${{ runner.os }}-bun-develop + + - name: 📩 Install dependencies and reload submodules + run: | + bun install --frozen-lockfile + bun run submodule-reload + + - name: đŸ’Ÿ Cache Gradle global + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle- + + - name: đŸ› ïž Generate project files + run: bun run prebuild + + - name: đŸ’Ÿ Cache project Gradle (.gradle) + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + with: + path: android/.gradle + key: ${{ runner.os }}-android-gradle-develop-${{ hashFiles('android/**/build.gradle', 'android/gradle/wrapper/gradle-wrapper.properties') }} + restore-keys: ${{ runner.os }}-android-gradle-develop + + - name: 🚀 Build APK + env: + EXPO_TV: 0 + run: bun run build:android:local + + - name: 📅 Set date tag + run: echo "DATE_TAG=$(date +%d-%m-%Y_%H-%M-%S)" >> $GITHUB_ENV + + - name: đŸ“€ Upload APK artifact + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: streamyfin-android-phone-apk-${{ env.DATE_TAG }} + path: | + android/app/build/outputs/apk/release/*.apk + retention-days: 7 + + build-android-tv: + if: (!contains(github.event.head_commit.message, '[skip ci]')) + runs-on: ubuntu-24.04 + name: đŸ€– Build Android APK (TV) + permissions: + contents: read + + steps: + - name: đŸ“„ Checkout code + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + ref: ${{ github.event.pull_request.head.sha || github.sha }} + fetch-depth: 0 + submodules: recursive + show-progress: false + + - name: 🍞 Setup Bun + uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76 # v2.0.2 + with: + bun-version: latest + + - name: đŸ’Ÿ Cache Bun dependencies + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + with: + path: ~/.bun/install/cache + key: ${{ runner.os }}-${{ runner.arch }}-bun-develop-${{ hashFiles('bun.lock') }} + restore-keys: | + ${{ runner.os }}-${{ runner.arch }}-bun-develop + ${{ runner.os }}-bun-develop + + - name: 📩 Install dependencies and reload submodules + run: | + bun install --frozen-lockfile + bun run submodule-reload + + - name: đŸ’Ÿ Cache Gradle global + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle- + + - name: đŸ› ïž Generate project files + run: bun run prebuild:tv + + - name: đŸ’Ÿ Cache project Gradle (.gradle) + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + with: + path: android/.gradle + key: ${{ runner.os }}-android-gradle-develop-${{ hashFiles('android/**/build.gradle', 'android/gradle/wrapper/gradle-wrapper.properties') }} + restore-keys: ${{ runner.os }}-android-gradle-develop + + - name: 🚀 Build APK + env: + EXPO_TV: 1 + run: bun run build:android:local + + - name: 📅 Set date tag + run: echo "DATE_TAG=$(date +%d-%m-%Y_%H-%M-%S)" >> $GITHUB_ENV + + - name: đŸ“€ Upload APK artifact + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: streamyfin-android-tv-apk-${{ env.DATE_TAG }} + path: | + android/app/build/outputs/apk/release/*.apk + retention-days: 7 + + build-ios-phone: + if: (!contains(github.event.head_commit.message, '[skip ci]') && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'streamyfin/streamyfin')) + runs-on: macos-15 + name: 🍎 Build iOS IPA (Phone) + permissions: + contents: read + + steps: + - name: đŸ“„ Checkout code + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + ref: ${{ github.event.pull_request.head.sha || github.sha }} + fetch-depth: 0 + submodules: recursive + show-progress: false + + - name: 🍞 Setup Bun + uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76 # v2.0.2 + with: + bun-version: latest + + - name: đŸ’Ÿ Cache Bun dependencies + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + with: + path: ~/.bun/install/cache + key: ${{ runner.os }}-bun-cache-${{ hashFiles('bun.lock') }} + restore-keys: | + ${{ runner.os }}-bun-cache + + - name: 📩 Install dependencies and reload submodules + run: | + bun install --frozen-lockfile + bun run submodule-reload + + - name: đŸ› ïž Generate project files + run: bun run prebuild + + - name: đŸ—ïž Setup EAS + uses: expo/expo-github-action@main + with: + eas-version: latest + token: ${{ secrets.EXPO_TOKEN }} + eas-cache: true + + - name: ⚙ Ensure iOS SDKs installed + run: xcodebuild -downloadPlatform iOS + + - name: 🚀 Build iOS app + env: + EXPO_TV: 0 + run: eas build -p ios --local --non-interactive + + - name: 📅 Set date tag + run: echo "DATE_TAG=$(date +%d-%m-%Y_%H-%M-%S)" >> $GITHUB_ENV + + - name: đŸ“€ Upload IPA artifact + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: streamyfin-ios-phone-ipa-${{ env.DATE_TAG }} + path: build-*.ipa + retention-days: 7 + + # Disabled for now - uncomment when ready to build iOS TV + # build-ios-tv: + # if: (!contains(github.event.head_commit.message, '[skip ci]') && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'streamyfin/streamyfin')) + # runs-on: macos-15 + # name: 🍎 Build iOS IPA (TV) + # permissions: + # contents: read + # + # steps: + # - name: đŸ“„ Checkout code + # uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + # with: + # ref: ${{ github.event.pull_request.head.sha || github.sha }} + # fetch-depth: 0 + # submodules: recursive + # show-progress: false + # + # - name: 🍞 Setup Bun + # uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76 # v2.0.2 + # with: + # bun-version: latest + # + # - name: đŸ’Ÿ Cache Bun dependencies + # uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + # with: + # path: ~/.bun/install/cache + # key: ${{ runner.os }}-bun-cache-${{ hashFiles('bun.lock') }} + # restore-keys: | + # ${{ runner.os }}-bun-cache + # + # - name: 📩 Install dependencies and reload submodules + # run: | + # bun install --frozen-lockfile + # bun run submodule-reload + # + # - name: đŸ› ïž Generate project files + # run: bun run prebuild:tv + # + # - name: đŸ—ïž Setup EAS + # uses: expo/expo-github-action@main + # with: + # eas-version: latest + # token: ${{ secrets.EXPO_TOKEN }} + # eas-cache: true + # + # - name: ⚙ Ensure tvOS SDKs installed + # run: xcodebuild -downloadPlatform tvOS + # + # - name: 🚀 Build iOS app + # env: + # EXPO_TV: 1 + # run: eas build -p ios --local --non-interactive + # + # - name: 📅 Set date tag + # run: echo "DATE_TAG=$(date +%d-%m-%Y_%H-%M-%S)" >> $GITHUB_ENV + # + # - name: đŸ“€ Upload IPA artifact + # uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + # with: + # name: streamyfin-ios-tv-ipa-${{ env.DATE_TAG }} + # path: build-*.ipa + # retention-days: 7 diff --git a/.github/workflows/build-ios.yml b/.github/workflows/build-ios.yml deleted file mode 100644 index 76e4a4d2..00000000 --- a/.github/workflows/build-ios.yml +++ /dev/null @@ -1,95 +0,0 @@ -name: đŸ€– iOS IPA Build (Phone + TV) - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -on: - workflow_dispatch: - pull_request: - branches: [develop, master] - paths-ignore: - - "*.md" - push: - branches: [develop, master] - paths-ignore: - - "*.md" - -jobs: - build-ios: - if: (!contains(github.event.head_commit.message, '[skip ci]') && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'streamyfin/streamyfin')) - runs-on: macos-15 - name: đŸ—ïž Build iOS IPA - permissions: - contents: read - - strategy: - fail-fast: false - matrix: - target: [phone] - # target: [phone, tv] - - steps: - - name: đŸ“„ Checkout code - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - with: - ref: ${{ github.event.pull_request.head.sha || github.sha }} - fetch-depth: 0 - submodules: recursive - show-progress: false - - - name: 🍞 Setup Bun - uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76 # v2.0.2 - with: - bun-version: latest - - - name: đŸ’Ÿ Cache Bun dependencies - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 - with: - path: ~/.bun/install/cache - key: ${{ runner.os }}-bun-cache-${{ hashFiles('bun.lock') }} - restore-keys: | - ${{ runner.os }}-bun-cache - - - name: 📩 Install dependencies and reload submodules - run: | - bun install --frozen-lockfile - bun run submodule-reload - - - name: đŸ› ïž Generate project files - run: | - if [ "${{ matrix.target }}" = "tv" ]; then - bun run prebuild:tv - else - bun run prebuild - fi - - - name: đŸ—ïž Setup EAS - uses: expo/expo-github-action@main - with: - eas-version: latest - token: ${{ secrets.EXPO_TOKEN }} - eas-cache: true - - - name: ⚙ Ensure iOS/tvOS SDKs installed - run: | - if [ "${{ matrix.target }}" = "tv" ]; then - xcodebuild -downloadPlatform tvOS - else - xcodebuild -downloadPlatform iOS - fi - - - name: 🚀 Build iOS app - env: - EXPO_TV: ${{ matrix.target == 'tv' && 1 || 0 }} - run: eas build -p ios --local --non-interactive - - - name: 📅 Set date tag - run: echo "DATE_TAG=$(date +%d-%m-%Y_%H-%M-%S)" >> $GITHUB_ENV - - - name: đŸ“€ Upload IPA artifact - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 - with: - name: streamyfin-ios-${{ matrix.target }}-ipa-${{ env.DATE_TAG }} - path: build-*.ipa - retention-days: 7 From 312a59c5b0a1369b7aa27374284d2cdf9a3fa0a9 Mon Sep 17 00:00:00 2001 From: Uruk Date: Tue, 30 Sep 2025 01:42:17 +0200 Subject: [PATCH 17/29] refactor(ci): improve build workflow handling and artifact collection logic Restructures the GitHub Actions workflow to better handle consolidated vs separate build workflows. Changes the artifact collection to trigger on workflow completion rather than just success, improving visibility of failed builds. Adds explicit fallback logic for backward compatibility with separate Android and iOS workflows. Introduces artifact pattern matching for more reliable build target identification and adds special handling to disable iOS TV builds. Enhances debugging output to show which workflow type is being used and lists all discovered artifacts. --- .github/workflows/artifact-comment.yml | 133 +++++++++++++++++-------- 1 file changed, 90 insertions(+), 43 deletions(-) diff --git a/.github/workflows/artifact-comment.yml b/.github/workflows/artifact-comment.yml index 08710fe8..0e9eea6c 100644 --- a/.github/workflows/artifact-comment.yml +++ b/.github/workflows/artifact-comment.yml @@ -137,62 +137,105 @@ jobs: const latestAndroidRun = buildRuns.find(run => run.name.includes('Android APK Build')); const latestIOSRun = buildRuns.find(run => run.name.includes('iOS IPA Build')); - // Store status for unified workflow (preferred) or fallback to separate workflows - const appsWorkflowRun = latestAppsRun || latestAndroidRun; - if (appsWorkflowRun) { + // For the consolidated workflow, both Android and iOS share the same run + if (latestAppsRun) { + // Both platforms use same workflow run status buildStatuses['Android'] = { - name: appsWorkflowRun.name, - status: appsWorkflowRun.status, - conclusion: appsWorkflowRun.conclusion, - url: appsWorkflowRun.html_url, - runId: appsWorkflowRun.id, - created_at: appsWorkflowRun.created_at + name: latestAppsRun.name, + status: latestAppsRun.status, + conclusion: latestAppsRun.conclusion, + url: latestAppsRun.html_url, + runId: latestAppsRun.id, + created_at: latestAppsRun.created_at }; - // Collect artifacts if completed successfully - if (appsWorkflowRun.conclusion === 'success') { + buildStatuses['iOS'] = { + name: latestAppsRun.name, + status: latestAppsRun.status, + conclusion: latestAppsRun.conclusion, + url: latestAppsRun.html_url, + runId: latestAppsRun.id, + created_at: latestAppsRun.created_at + }; + + // Collect artifacts if workflow has completed (regardless of success/failure) + if (latestAppsRun.status === 'completed') { try { const { data: artifacts } = await github.rest.actions.listWorkflowRunArtifacts({ owner: context.repo.owner, repo: context.repo.repo, - run_id: appsWorkflowRun.id + run_id: latestAppsRun.id }); allArtifacts.push(...artifacts.artifacts); } catch (error) { - console.log(`Failed to get apps artifacts for run ${appsWorkflowRun.id}:`, error.message); + console.log(`Failed to get apps artifacts for run ${latestAppsRun.id}:`, error.message); + } + } + } else { + // Fallback to separate workflows (for backward compatibility) + if (latestAndroidRun) { + buildStatuses['Android'] = { + name: latestAndroidRun.name, + status: latestAndroidRun.status, + conclusion: latestAndroidRun.conclusion, + url: latestAndroidRun.html_url, + runId: latestAndroidRun.id, + created_at: latestAndroidRun.created_at + }; + + if (latestAndroidRun.conclusion === 'success') { + try { + const { data: artifacts } = await github.rest.actions.listWorkflowRunArtifacts({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: latestAndroidRun.id + }); + allArtifacts.push(...artifacts.artifacts); + } catch (error) { + console.log(`Failed to get Android artifacts for run ${latestAndroidRun.id}:`, error.message); + } } } - } - - // For iOS, use the same workflow run (since it's all in one now) or fallback to separate - const iosWorkflowRun = latestAppsRun || latestIOSRun; - if (iosWorkflowRun) { - buildStatuses['iOS'] = { - name: iosWorkflowRun.name, - status: iosWorkflowRun.status, - conclusion: iosWorkflowRun.conclusion, - url: iosWorkflowRun.html_url, - runId: iosWorkflowRun.id, - created_at: iosWorkflowRun.created_at - }; - // Only collect artifacts if not already collected from apps workflow - if (!latestAppsRun && iosWorkflowRun.conclusion === 'success') { - try { - const { data: artifacts } = await github.rest.actions.listWorkflowRunArtifacts({ - owner: context.repo.owner, - repo: context.repo.repo, - run_id: iosWorkflowRun.id - }); - allArtifacts.push(...artifacts.artifacts); - } catch (error) { - console.log(`Failed to get iOS artifacts for run ${iosWorkflowRun.id}:`, error.message); + if (latestIOSRun) { + buildStatuses['iOS'] = { + name: latestIOSRun.name, + status: latestIOSRun.status, + conclusion: latestIOSRun.conclusion, + url: latestIOSRun.html_url, + runId: latestIOSRun.id, + created_at: latestIOSRun.created_at + }; + + if (latestIOSRun.conclusion === 'success') { + try { + const { data: artifacts } = await github.rest.actions.listWorkflowRunArtifacts({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: latestIOSRun.id + }); + allArtifacts.push(...artifacts.artifacts); + } catch (error) { + console.log(`Failed to get iOS artifacts for run ${latestIOSRun.id}:`, error.message); + } } } } console.log(`Collected ${allArtifacts.length} total artifacts from all builds`); + // Debug: Show which workflow we're using and its status + if (latestAppsRun) { + console.log(`Using consolidated workflow: ${latestAppsRun.name} (${latestAppsRun.status}/${latestAppsRun.conclusion})`); + } else { + console.log(`Using separate workflows - Android: ${latestAndroidRun?.name || 'none'}, iOS: ${latestIOSRun?.name || 'none'}`); + } + + // Debug: List all artifacts found + allArtifacts.forEach(artifact => { + console.log(`- Artifact: ${artifact.name} (from run ${artifact.workflow_run.id})`); + }); + // Build comment body with progressive status for individual builds let commentBody = `## 🔧 Build Status for PR #${pr.number}\n\n`; commentBody += `🔗 **Commit**: [\`${targetCommitSha.substring(0, 7)}\`](https://github.com/${context.repo.owner}/${context.repo.repo}/commit/${targetCommitSha})\n\n`; // Progressive build status and downloads table @@ -202,10 +245,10 @@ jobs: // Process each expected build target individually const buildTargets = [ - { name: 'Android Phone', platform: 'đŸ€–', device: 'đŸ“±', workflowType: 'Android', target: 'phone' }, - { name: 'Android TV', platform: 'đŸ€–', device: 'đŸ“ș', workflowType: 'Android', target: 'tv' }, - { name: 'iOS Phone', platform: '🍎', device: 'đŸ“±', workflowType: 'iOS', target: 'phone' }, - { name: 'iOS TV', platform: '🍎', device: 'đŸ“ș', workflowType: 'iOS', target: 'tv' } + { name: 'Android Phone', platform: 'đŸ€–', device: 'đŸ“±', workflowType: 'Android', target: 'phone', artifactPattern: /android.*phone/i }, + { name: 'Android TV', platform: 'đŸ€–', device: 'đŸ“ș', workflowType: 'Android', target: 'tv', artifactPattern: /android.*tv/i }, + { name: 'iOS Phone', platform: '🍎', device: 'đŸ“±', workflowType: 'iOS', target: 'phone', artifactPattern: /ios.*phone/i }, + { name: 'iOS TV', platform: '🍎', device: 'đŸ“ș', workflowType: 'iOS', target: 'tv', artifactPattern: /ios.*tv/i } ]; for (const target of buildTargets) { @@ -214,13 +257,17 @@ jobs: // Find matching artifact const matchingArtifact = allArtifacts.find(artifact => - target.pattern.test(artifact.name) + target.artifactPattern.test(artifact.name) ); let status = '⏳ Pending'; let downloadLink = '*Waiting for build...*'; - if (matchingStatus) { + // Special case for iOS TV - show as disabled + if (target.name === 'iOS TV') { + status = 'đŸ’€ Disabled'; + downloadLink = '*Disabled for now*'; + } else if (matchingStatus) { if (matchingStatus.conclusion === 'success' && matchingArtifact) { status = '✅ Complete'; const nightlyLink = `https://nightly.link/${context.repo.owner}/${context.repo.repo}/actions/runs/${matchingArtifact.workflow_run.id}/${matchingArtifact.name}.zip`; From 0a41962ddfd0f9c4f2553ba4b906febe28bbf90f Mon Sep 17 00:00:00 2001 From: Uruk Date: Tue, 30 Sep 2025 01:49:38 +0200 Subject: [PATCH 18/29] fix: improve build status handling and artifact links Replaces nightly.link with direct GitHub artifact URLs for better reliability. Adds handling for cancelled builds and edge cases where workflows complete but artifacts aren't immediately available or conclusions are pending. Improves status messages to provide more detailed information for unexpected build states. --- .github/workflows/artifact-comment.yml | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/.github/workflows/artifact-comment.yml b/.github/workflows/artifact-comment.yml index 0e9eea6c..61a94b0a 100644 --- a/.github/workflows/artifact-comment.yml +++ b/.github/workflows/artifact-comment.yml @@ -270,22 +270,33 @@ jobs: } else if (matchingStatus) { if (matchingStatus.conclusion === 'success' && matchingArtifact) { status = '✅ Complete'; - const nightlyLink = `https://nightly.link/${context.repo.owner}/${context.repo.repo}/actions/runs/${matchingArtifact.workflow_run.id}/${matchingArtifact.name}.zip`; + const directLink = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${matchingArtifact.workflow_run.id}/artifacts/${matchingArtifact.id}`; const fileType = target.name.includes('Android') ? 'APK' : 'IPA'; - downloadLink = `[đŸ“„ Download ${fileType}](${nightlyLink})`; + downloadLink = `[đŸ“„ Download ${fileType}](${directLink})`; } else if (matchingStatus.conclusion === 'failure') { status = `❌ [Failed](${matchingStatus.url})`; downloadLink = '*Build failed*'; + } else if (matchingStatus.conclusion === 'cancelled') { + status = `âšȘ [Cancelled](${matchingStatus.url})`; + downloadLink = '*Build cancelled*'; } else if (matchingStatus.status === 'in_progress') { status = `🔄 [Building...](${matchingStatus.url})`; downloadLink = '*Build in progress...*'; } else if (matchingStatus.status === 'queued') { status = `⏳ [Queued](${matchingStatus.url})`; downloadLink = '*Waiting to start...*'; + } else if (matchingStatus.status === 'completed' && !matchingStatus.conclusion) { + // Workflow completed but conclusion not yet available (rare edge case) + status = `🔄 [Finishing...](${matchingStatus.url})`; + downloadLink = '*Finalizing build...*'; + } else if (matchingStatus.status === 'completed' && matchingStatus.conclusion === 'success' && !matchingArtifact) { + // Build succeeded but artifacts not yet available + status = `⏳ [Processing artifacts...](${matchingStatus.url})`; + downloadLink = '*Preparing download...*'; } else { - // Show any other status with timestamp for debugging - status = `🔄 [${matchingStatus.status}](${matchingStatus.url})`; - downloadLink = `*Status: ${matchingStatus.status}*`; + // Fallback for any unexpected states + status = `❓ [${matchingStatus.status}/${matchingStatus.conclusion || 'pending'}](${matchingStatus.url})`; + downloadLink = `*Status: ${matchingStatus.status}, Conclusion: ${matchingStatus.conclusion || 'pending'}*`; } } From 71a3c5e92bfbea1e84cf8c901544a0a125b3995f Mon Sep 17 00:00:00 2001 From: Uruk Date: Tue, 30 Sep 2025 01:57:18 +0200 Subject: [PATCH 19/29] feat: improve GitHub workflow status tracking Enhances artifact comment workflow to provide more accurate build status reporting by tracking individual job statuses within consolidated workflows rather than using workflow-level status. Excludes cancelled workflow runs from consideration and prioritizes active runs over completed ones when determining build status. Maps specific job names to build targets (Android Phone, Android TV, iOS Phone) to provide granular status information for each platform and device combination. Improves artifact collection logic to gather artifacts when any job completes successfully, not just when entire workflow completes. --- .github/workflows/artifact-comment.yml | 129 ++++++++++++++++++------- 1 file changed, 94 insertions(+), 35 deletions(-) diff --git a/.github/workflows/artifact-comment.yml b/.github/workflows/artifact-comment.yml index 61a94b0a..0a8cc576 100644 --- a/.github/workflows/artifact-comment.yml +++ b/.github/workflows/artifact-comment.yml @@ -112,54 +112,113 @@ jobs: per_page: 30 }); - // Filter for build workflows only and sort by creation time (most recent first) + // Filter for build workflows only, exclude cancelled runs, and sort by creation time (most recent first) const buildRuns = workflowRuns.workflow_runs .filter(run => - run.name.includes('Build Apps') || + (run.name.includes('Build Apps') || run.name.includes('Android APK Build') || - run.name.includes('iOS IPA Build') + run.name.includes('iOS IPA Build')) && + run.conclusion !== 'cancelled' // Ignore cancelled runs ) .sort((a, b) => new Date(b.created_at) - new Date(a.created_at)); - console.log(`Found ${buildRuns.length} build workflow runs for this commit`); + console.log(`Found ${buildRuns.length} non-cancelled build workflow runs for this commit`); // Log current status of each build for debugging buildRuns.forEach(run => { console.log(`- ${run.name}: ${run.status} (${run.conclusion || 'no conclusion yet'}) - Created: ${run.created_at}`); }); - // Collect artifacts and statuses from builds - get most recent run for each workflow type + // Collect artifacts and statuses from builds - prioritize active runs over completed ones let allArtifacts = []; let buildStatuses = {}; - // Get the most recent run for the unified apps workflow - const latestAppsRun = buildRuns.find(run => run.name.includes('Build Apps')); - const latestAndroidRun = buildRuns.find(run => run.name.includes('Android APK Build')); - const latestIOSRun = buildRuns.find(run => run.name.includes('iOS IPA Build')); + // Get the most relevant run for each workflow type (prioritize in_progress over completed) + const findBestRun = (nameFilter) => { + const matchingRuns = buildRuns.filter(run => run.name.includes(nameFilter)); + // First try to find an in-progress run + const inProgressRun = matchingRuns.find(run => run.status === 'in_progress'); + if (inProgressRun) return inProgressRun; + // Then try to find a queued run + const queuedRun = matchingRuns.find(run => run.status === 'queued'); + if (queuedRun) return queuedRun; + // Finally fall back to most recent completed run + return matchingRuns[0]; // Already sorted by most recent first + }; - // For the consolidated workflow, both Android and iOS share the same run + const latestAppsRun = findBestRun('Build Apps'); + const latestAndroidRun = findBestRun('Android APK Build'); + const latestIOSRun = findBestRun('iOS IPA Build'); + + // For the consolidated workflow, get individual job statuses if (latestAppsRun) { - // Both platforms use same workflow run status - buildStatuses['Android'] = { - name: latestAppsRun.name, - status: latestAppsRun.status, - conclusion: latestAppsRun.conclusion, - url: latestAppsRun.html_url, - runId: latestAppsRun.id, - created_at: latestAppsRun.created_at - }; + console.log(`Getting individual job statuses for run ${latestAppsRun.id}`); - buildStatuses['iOS'] = { - name: latestAppsRun.name, - status: latestAppsRun.status, - conclusion: latestAppsRun.conclusion, - url: latestAppsRun.html_url, - runId: latestAppsRun.id, - created_at: latestAppsRun.created_at - }; + try { + // Get all jobs for this workflow run + const { data: jobs } = await github.rest.actions.listJobsForWorkflowRun({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: latestAppsRun.id + }); + + console.log(`Found ${jobs.jobs.length} jobs in workflow run`); + jobs.jobs.forEach(job => { + console.log(`- Job: ${job.name} | Status: ${job.status} | Conclusion: ${job.conclusion || 'none'}`); + }); + + // Map job names to our build targets + const jobMappings = { + 'Android Phone': ['đŸ€– Build Android APK (Phone)', 'build-android-phone'], + 'Android TV': ['đŸ€– Build Android APK (TV)', 'build-android-tv'], + 'iOS Phone': ['🍎 Build iOS IPA (Phone)', 'build-ios-phone'] + }; + + // Create individual status for each job + for (const [platform, jobNames] of Object.entries(jobMappings)) { + const job = jobs.jobs.find(j => + jobNames.some(name => j.name.includes(name) || j.name === name) + ); + + if (job) { + buildStatuses[platform] = { + name: job.name, + status: job.status, + conclusion: job.conclusion, + url: job.html_url, + runId: latestAppsRun.id, + created_at: job.started_at || latestAppsRun.created_at + }; + console.log(`Mapped ${platform} to job: ${job.name} (${job.status}/${job.conclusion || 'none'})`); + } else { + console.log(`No job found for ${platform}, using workflow status as fallback`); + buildStatuses[platform] = { + name: latestAppsRun.name, + status: latestAppsRun.status, + conclusion: latestAppsRun.conclusion, + url: latestAppsRun.html_url, + runId: latestAppsRun.id, + created_at: latestAppsRun.created_at + }; + } + } + + } catch (error) { + console.log(`Failed to get jobs for run ${latestAppsRun.id}:`, error.message); + // Fallback to workflow-level status + buildStatuses['Android Phone'] = buildStatuses['Android TV'] = buildStatuses['iOS Phone'] = { + name: latestAppsRun.name, + status: latestAppsRun.status, + conclusion: latestAppsRun.conclusion, + url: latestAppsRun.html_url, + runId: latestAppsRun.id, + created_at: latestAppsRun.created_at + }; + } - // Collect artifacts if workflow has completed (regardless of success/failure) - if (latestAppsRun.status === 'completed') { + // Collect artifacts if any job has completed successfully + if (latestAppsRun.status === 'completed' || + Object.values(buildStatuses).some(status => status.conclusion === 'success')) { try { const { data: artifacts } = await github.rest.actions.listWorkflowRunArtifacts({ owner: context.repo.owner, @@ -245,15 +304,15 @@ jobs: // Process each expected build target individually const buildTargets = [ - { name: 'Android Phone', platform: 'đŸ€–', device: 'đŸ“±', workflowType: 'Android', target: 'phone', artifactPattern: /android.*phone/i }, - { name: 'Android TV', platform: 'đŸ€–', device: 'đŸ“ș', workflowType: 'Android', target: 'tv', artifactPattern: /android.*tv/i }, - { name: 'iOS Phone', platform: '🍎', device: 'đŸ“±', workflowType: 'iOS', target: 'phone', artifactPattern: /ios.*phone/i }, - { name: 'iOS TV', platform: '🍎', device: 'đŸ“ș', workflowType: 'iOS', target: 'tv', artifactPattern: /ios.*tv/i } + { name: 'Android Phone', platform: 'đŸ€–', device: 'đŸ“±', statusKey: 'Android Phone', artifactPattern: /android.*phone/i }, + { name: 'Android TV', platform: 'đŸ€–', device: 'đŸ“ș', statusKey: 'Android TV', artifactPattern: /android.*tv/i }, + { name: 'iOS Phone', platform: '🍎', device: 'đŸ“±', statusKey: 'iOS Phone', artifactPattern: /ios.*phone/i }, + { name: 'iOS TV', platform: '🍎', device: 'đŸ“ș', statusKey: 'iOS TV', artifactPattern: /ios.*tv/i } ]; for (const target of buildTargets) { - // Find matching workflow status - const matchingStatus = buildStatuses[target.workflowType]; + // Find matching job status directly + const matchingStatus = buildStatuses[target.statusKey]; // Find matching artifact const matchingArtifact = allArtifacts.find(artifact => From c6ad06b0843fb90d7e06f3c443aee0056888fbc3 Mon Sep 17 00:00:00 2001 From: Uruk Date: Tue, 30 Sep 2025 02:15:25 +0200 Subject: [PATCH 20/29] fix: improve workflow run detection for cancelled jobs Enhances the artifact comment workflow to better handle cancelled workflow runs by checking individual job statuses instead of dismissing entire workflows. Previously, workflows marked as cancelled at the top level were completely ignored, even if some jobs within them were still running or completed successfully. Now prioritizes active jobs over cancelled ones and validates that at least one job is actually running before processing a workflow run, preventing false negatives while still filtering out truly cancelled workflows. --- .github/workflows/artifact-comment.yml | 38 ++++++++++++++++++++++---- 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/.github/workflows/artifact-comment.yml b/.github/workflows/artifact-comment.yml index 0a8cc576..d032dd67 100644 --- a/.github/workflows/artifact-comment.yml +++ b/.github/workflows/artifact-comment.yml @@ -112,13 +112,12 @@ jobs: per_page: 30 }); - // Filter for build workflows only, exclude cancelled runs, and sort by creation time (most recent first) + // Filter for build workflows only, include active runs even if marked as cancelled const buildRuns = workflowRuns.workflow_runs .filter(run => (run.name.includes('Build Apps') || run.name.includes('Android APK Build') || - run.name.includes('iOS IPA Build')) && - run.conclusion !== 'cancelled' // Ignore cancelled runs + run.name.includes('iOS IPA Build')) ) .sort((a, b) => new Date(b.created_at) - new Date(a.created_at)); @@ -133,16 +132,28 @@ jobs: let allArtifacts = []; let buildStatuses = {}; - // Get the most relevant run for each workflow type (prioritize in_progress over completed) + // Get the most relevant run for each workflow type (prioritize active over cancelled) const findBestRun = (nameFilter) => { const matchingRuns = buildRuns.filter(run => run.name.includes(nameFilter)); + // First try to find an in-progress run const inProgressRun = matchingRuns.find(run => run.status === 'in_progress'); if (inProgressRun) return inProgressRun; + // Then try to find a queued run const queuedRun = matchingRuns.find(run => run.status === 'queued'); if (queuedRun) return queuedRun; - // Finally fall back to most recent completed run + + // Check if the workflow is completed but has non-cancelled jobs + const completedRuns = matchingRuns.filter(run => run.status === 'completed'); + for (const run of completedRuns) { + // We'll check individual jobs later to see if they're actually running + if (run.conclusion !== 'cancelled') { + return run; + } + } + + // Finally fall back to most recent run (even if cancelled at workflow level) return matchingRuns[0]; // Already sorted by most recent first }; @@ -152,7 +163,7 @@ jobs: // For the consolidated workflow, get individual job statuses if (latestAppsRun) { - console.log(`Getting individual job statuses for run ${latestAppsRun.id}`); + console.log(`Getting individual job statuses for run ${latestAppsRun.id} (status: ${latestAppsRun.status}, conclusion: ${latestAppsRun.conclusion || 'none'})`); try { // Get all jobs for this workflow run @@ -167,6 +178,21 @@ jobs: console.log(`- Job: ${job.name} | Status: ${job.status} | Conclusion: ${job.conclusion || 'none'}`); }); + // Check if we have any actually running jobs (not cancelled) + const activeJobs = jobs.jobs.filter(job => + job.status === 'in_progress' || + job.status === 'queued' || + (job.status === 'completed' && job.conclusion !== 'cancelled') + ); + + console.log(`Found ${activeJobs.length} active (non-cancelled) jobs out of ${jobs.jobs.length} total jobs`); + + // If no jobs are actually running, skip this workflow + if (activeJobs.length === 0 && latestAppsRun.conclusion === 'cancelled') { + console.log('All jobs are cancelled, skipping this workflow run'); + return; // Exit early + } + // Map job names to our build targets const jobMappings = { 'Android Phone': ['đŸ€– Build Android APK (Phone)', 'build-android-phone'], From e877d038ba07e550c52a13946e051d36ef6ca22d Mon Sep 17 00:00:00 2001 From: Lance Chant <13349722+lancechant@users.noreply.github.com> Date: Tue, 30 Sep 2025 10:39:56 +0200 Subject: [PATCH 21/29] fix: download handling of "other" media Fixed an issue where downloaded items were not appearing in downloads Signed-off-by: Lance Chant <13349722+lancechant@users.noreply.github.com> --- app/(auth)/(tabs)/(home)/downloads/index.tsx | 70 +++++++++++++++++++- components/downloads/MovieCard.tsx | 2 +- providers/DownloadProvider.tsx | 29 ++++++-- providers/Downloads/types.ts | 2 + translations/en.json | 4 ++ 5 files changed, 99 insertions(+), 8 deletions(-) diff --git a/app/(auth)/(tabs)/(home)/downloads/index.tsx b/app/(auth)/(tabs)/(home)/downloads/index.tsx index 1bd33ba2..07d3dbc5 100644 --- a/app/(auth)/(tabs)/(home)/downloads/index.tsx +++ b/app/(auth)/(tabs)/(home)/downloads/index.tsx @@ -90,6 +90,19 @@ export default function page() { } }, [downloadedFiles]); + const otherMedia = useMemo(() => { + try { + return ( + downloadedFiles?.filter( + (f) => f.item.Type !== "Movie" && f.item.Type !== "Episode", + ) || [] + ); + } catch { + setShowMigration(true); + return []; + } + }, [downloadedFiles]); + useEffect(() => { navigation.setOptions({ headerRight: () => ( @@ -128,8 +141,30 @@ export default function page() { writeToLog("ERROR", reason); toast.error(t("home.downloads.toasts.failed_to_delete_all_tvseries")); }); + const deleteOtherMedia = () => + Promise.all( + otherMedia.map((item) => + deleteFileByType(item.item.Type) + .then(() => + toast.success( + t("home.downloads.toasts.deleted_media_successfully", { + type: item.item.Type, + }), + ), + ) + .catch((reason) => { + writeToLog("ERROR", reason); + toast.error( + t("home.downloads.toasts.failed_to_delete_media", { + type: item.item.Type, + }), + ); + }), + ), + ); + const deleteAllMedia = async () => - await Promise.all([deleteMovies(), deleteShows()]); + await Promise.all([deleteMovies(), deleteShows(), deleteOtherMedia()]); return ( <> @@ -238,6 +273,34 @@ export default function page() { )} + + {otherMedia.length > 0 && ( + + + + {t("home.downloads.other_media")} + + + + {otherMedia?.length} + + + + + + {otherMedia?.map((item) => ( + + + + ))} + + + + )} {downloadedFiles?.length === 0 && ( @@ -273,6 +336,11 @@ export default function page() { + {otherMedia.length > 0 && ( + + )} diff --git a/components/downloads/MovieCard.tsx b/components/downloads/MovieCard.tsx index e9c8ab97..b0a5d555 100644 --- a/components/downloads/MovieCard.tsx +++ b/components/downloads/MovieCard.tsx @@ -37,7 +37,7 @@ export const MovieCard: React.FC = ({ item }) => { */ const handleDeleteFile = useCallback(() => { if (item.Id) { - deleteFile(item.Id, "Movie"); + deleteFile(item.Id, item.Type); } }, [deleteFile, item.Id]); diff --git a/providers/DownloadProvider.tsx b/providers/DownloadProvider.tsx index 4306b8d6..ffb15e9e 100644 --- a/providers/DownloadProvider.tsx +++ b/providers/DownloadProvider.tsx @@ -301,7 +301,7 @@ function useDownloadProvider() { return db.movies[id]; } - // If not in movies, check episodes + // Check episodes for (const series of Object.values(db.series)) { for (const season of Object.values(series.seasons)) { for (const episode of Object.values(season.episodes)) { @@ -312,6 +312,11 @@ function useDownloadProvider() { } } + // Check other media types + if (db.other[id]) { + return db.other[id]; + } + return undefined; }; @@ -348,7 +353,7 @@ function useDownloadProvider() { if (file) { return JSON.parse(file) as DownloadsDatabase; } - return { movies: {}, series: {} }; + return { movies: {}, series: {}, other: {} }; // Initialize other media types storage }; const getDownloadedItems = () => { @@ -360,6 +365,7 @@ function useDownloadProvider() { Object.values(season.episodes), ), ), + ...Object.values(db.other), // Include other media types in results ]; return allItems; }; @@ -658,6 +664,9 @@ function useDownloadProvider() { db.series[item.SeriesId].seasons[seasonNumber].episodes[ episodeNumber ] = downloadedItem; + } else if (item.Id) { + // Handle other media types + db.other[item.Id] = downloadedItem; } await saveDownloadsDatabase(db); @@ -856,16 +865,16 @@ function useDownloadProvider() { [authHeader, startDownload], ); - const deleteFile = async (id: string, type: "Movie" | "Episode") => { + const deleteFile = async (id: string, type: BaseItemDto["Type"]) => { const db = getDownloadsDatabase(); let downloadedItem: DownloadedItem | undefined; - if (type === "Movie") { + if (type === "Movie" && Object.entries(db.movies).length !== 0) { downloadedItem = db.movies[id]; if (downloadedItem) { delete db.movies[id]; } - } else if (type === "Episode") { + } else if (type === "Episode" && Object.entries(db.series).length !== 0) { const cleanUpEmptyParents = ( series: any, seasonNumber: string, @@ -895,6 +904,12 @@ function useDownloadProvider() { } if (downloadedItem) break; } + } else { + // Handle other media types + downloadedItem = db.other[id]; + if (downloadedItem) { + delete db.other[id]; + } } if (downloadedItem?.videoFilePath) { @@ -928,7 +943,7 @@ function useDownloadProvider() { const deleteItems = async (items: BaseItemDto[]) => { for (const item of items) { - if (item.Id && (item.Type === "Movie" || item.Type === "Episode")) { + if (item.Id) { await deleteFile(item.Id, item.Type); } } @@ -970,6 +985,8 @@ function useDownloadProvider() { const db = getDownloadsDatabase(); if (db.movies[itemId]) { db.movies[itemId] = updatedItem; + } else if (db.other[itemId]) { + db.other[itemId] = updatedItem; } else { for (const series of Object.values(db.series)) { for (const season of Object.values(series.seasons)) { diff --git a/providers/Downloads/types.ts b/providers/Downloads/types.ts index cff87ddf..8eb6833f 100644 --- a/providers/Downloads/types.ts +++ b/providers/Downloads/types.ts @@ -88,6 +88,8 @@ export interface DownloadsDatabase { movies: Record; /** A map of series IDs to their downloaded series data. */ series: Record; + /** A map of IDs to downloaded items that are neither movies nor episodes */ + other: Record; } /** diff --git a/translations/en.json b/translations/en.json index 8578567a..82723427 100644 --- a/translations/en.json +++ b/translations/en.json @@ -237,12 +237,14 @@ "tvseries": "TV-Series", "movies": "Movies", "queue": "Queue", + "other_media": "Other media", "queue_hint": "Queue and downloads will be lost on app restart", "no_items_in_queue": "No Items in Queue", "no_downloaded_items": "No Downloaded Items", "delete_all_movies_button": "Delete All Movies", "delete_all_tvseries_button": "Delete All TV-Series", "delete_all_button": "Delete All", + "delete_all_other_media_button": "Delete other media", "active_download": "Active Download", "no_active_downloads": "No Active Downloads", "active_downloads": "Active Downloads", @@ -259,6 +261,8 @@ "failed_to_delete_all_movies": "Failed to Delete All Movies", "deleted_all_tvseries_successfully": "Deleted All TV-Series Successfully!", "failed_to_delete_all_tvseries": "Failed to Delete All TV-Series", + "deleted_media_successfully": "Deleted other media Successfully!", + "failed_to_delete_media": "Failed to Delete other media", "download_deleted": "Download Deleted", "could_not_delete_download": "Could Not Delete Download", "download_paused": "Download Paused", From 1eba074ebd1db09ea2ea860ea61c2f2cdb40481c Mon Sep 17 00:00:00 2001 From: Lance Chant <13349722+lancechant@users.noreply.github.com> Date: Tue, 30 Sep 2025 11:18:36 +0200 Subject: [PATCH 22/29] chore: rework logic Changed logic to use the settings component to store the changed values rather than referencing storage directly Deleted an unused file Signed-off-by: Lance Chant <13349722+lancechant@users.noreply.github.com> --- app/(auth)/player/direct-player.tsx | 18 ++- components/settings/SubtitleToggles.tsx | 78 ++++-------- components/settings/VLCSubtitleSettings.tsx | 131 -------------------- utils/atoms/settings.ts | 14 +++ 4 files changed, 45 insertions(+), 196 deletions(-) delete mode 100644 components/settings/VLCSubtitleSettings.tsx diff --git a/app/(auth)/player/direct-player.tsx b/app/(auth)/player/direct-player.tsx index a8caf201..1aab148a 100644 --- a/app/(auth)/player/direct-player.tsx +++ b/app/(auth)/player/direct-player.tsx @@ -44,7 +44,6 @@ import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; import { useSettings } from "@/utils/atoms/settings"; import { getStreamUrl } from "@/utils/jellyfin/media/getStreamUrl"; import { writeToLog } from "@/utils/log"; -import { storage } from "@/utils/mmkv"; import { generateDeviceProfile } from "@/utils/profiles/native"; import { msToTicks, ticksToSeconds } from "@/utils/time"; @@ -566,18 +565,15 @@ export default function page() { initOptions.push(`--sub-track=${finalIndex}`); // Add VLC subtitle styling options from settings - const textColor = (storage.getString("vlc.textColor") || - "White") as VLCColor; - const backgroundColor = (storage.getString("vlc.backgroundColor") || + const textColor = (settings.vlcTextColor ?? "White") as VLCColor; + const backgroundColor = (settings.vlcBackgroundColor ?? "Black") as VLCColor; - const outlineColor = (storage.getString("vlc.outlineColor") || - "Black") as VLCColor; - const outlineThickness = (storage.getString("vlc.outlineThickness") || + const outlineColor = (settings.vlcOutlineColor ?? "Black") as VLCColor; + const outlineThickness = (settings.vlcOutlineThickness ?? "Normal") as OutlineThickness; - const backgroundOpacity = storage.getNumber("vlc.backgroundOpacity") || 128; - const outlineOpacity = storage.getNumber("vlc.outlineOpacity") || 255; - const isBold = storage.getBoolean("vlc.isBold") || false; - + const backgroundOpacity = settings.vlcBackgroundOpacity ?? 128; + const outlineOpacity = settings.vlcOutlineOpacity ?? 255; + const isBold = settings.vlcIsBold ?? false; // Add subtitle styling options initOptions.push(`--freetype-color=${VLC_COLORS[textColor]}`); initOptions.push(`--freetype-background-opacity=${backgroundOpacity}`); diff --git a/components/settings/SubtitleToggles.tsx b/components/settings/SubtitleToggles.tsx index d1d30274..f01ab200 100644 --- a/components/settings/SubtitleToggles.tsx +++ b/components/settings/SubtitleToggles.tsx @@ -1,4 +1,3 @@ -import { useEffect, useState } from "react"; import { Platform, TouchableOpacity, View, type ViewProps } from "react-native"; const _DropdownMenu = !Platform.isTV ? require("zeego/dropdown-menu") : null; @@ -10,7 +9,6 @@ import { Switch } from "react-native-gesture-handler"; import Dropdown from "@/components/common/Dropdown"; import { Stepper } from "@/components/inputs/Stepper"; import { useSettings } from "@/utils/atoms/settings"; -import { storage } from "@/utils/mmkv"; import { Text } from "../common/Text"; import { ListGroup } from "../list/ListGroup"; import { ListItem } from "../list/ListItem"; @@ -29,51 +27,14 @@ export const SubtitleToggles: React.FC = ({ ...props }) => { const cultures = media.cultures; const { t } = useTranslation(); - // VLC subtitle styling states - const [textColor, setTextColor] = useState( - storage.getString("vlc.textColor") || "White", - ); - const [backgroundColor, setBackgroundColor] = useState( - storage.getString("vlc.backgroundColor") || "Black", - ); - const [outlineColor, setOutlineColor] = useState( - storage.getString("vlc.outlineColor") || "Black", - ); - const [outlineThickness, setOutlineThickness] = useState( - storage.getString("vlc.outlineThickness") || "Normal", - ); - const [backgroundOpacity, setBackgroundOpacity] = useState( - storage.getNumber("vlc.backgroundOpacity") || 128, - ); - const [outlineOpacity, setOutlineOpacity] = useState( - storage.getNumber("vlc.outlineOpacity") || 255, - ); - const [isBold, setIsBold] = useState( - storage.getBoolean("vlc.isBold") || false, - ); - - // VLC settings effects - useEffect(() => { - storage.set("vlc.textColor", textColor); - }, [textColor]); - useEffect(() => { - storage.set("vlc.backgroundColor", backgroundColor); - }, [backgroundColor]); - useEffect(() => { - storage.set("vlc.outlineColor", outlineColor); - }, [outlineColor]); - useEffect(() => { - storage.set("vlc.outlineThickness", outlineThickness); - }, [outlineThickness]); - useEffect(() => { - storage.set("vlc.backgroundOpacity", backgroundOpacity); - }, [backgroundOpacity]); - useEffect(() => { - storage.set("vlc.outlineOpacity", outlineOpacity); - }, [outlineOpacity]); - useEffect(() => { - storage.set("vlc.isBold", isBold); - }, [isBold]); + // Get VLC subtitle settings from the settings system + const textColor = pluginSettings?.vlcTextColor ?? "White"; + const backgroundColor = pluginSettings?.vlcBackgroundColor ?? "Black"; + const outlineColor = pluginSettings?.vlcOutlineColor ?? "Black"; + const outlineThickness = pluginSettings?.vlcOutlineThickness ?? "Normal"; + const backgroundOpacity = pluginSettings?.vlcBackgroundOpacity ?? 128; + const outlineOpacity = pluginSettings?.vlcOutlineOpacity ?? 255; + const isBold = pluginSettings?.vlcIsBold ?? false; if (isTv) return null; if (!settings) return null; @@ -217,7 +178,7 @@ export const SubtitleToggles: React.FC = ({ ...props }) => { } label={t("home.settings.subtitles.text_color")} - onSelected={setTextColor} + onSelected={(value) => updateSettings({ vlcTextColor: value })} /> @@ -240,7 +201,9 @@ export const SubtitleToggles: React.FC = ({ ...props }) => { } label={t("home.settings.subtitles.background_color")} - onSelected={setBackgroundColor} + onSelected={(value) => + updateSettings({ vlcBackgroundColor: value }) + } /> @@ -263,7 +226,7 @@ export const SubtitleToggles: React.FC = ({ ...props }) => { } label={t("home.settings.subtitles.outline_color")} - onSelected={setOutlineColor} + onSelected={(value) => updateSettings({ vlcOutlineColor: value })} /> @@ -286,7 +249,9 @@ export const SubtitleToggles: React.FC = ({ ...props }) => { } label={t("home.settings.subtitles.outline_thickness")} - onSelected={setOutlineThickness} + onSelected={(value) => + updateSettings({ vlcOutlineThickness: value }) + } /> @@ -305,7 +270,9 @@ export const SubtitleToggles: React.FC = ({ ...props }) => { } label={t("home.settings.subtitles.background_opacity")} - onSelected={setBackgroundOpacity} + onSelected={(value) => + updateSettings({ vlcBackgroundOpacity: value }) + } /> @@ -324,11 +291,14 @@ export const SubtitleToggles: React.FC = ({ ...props }) => { } label={t("home.settings.subtitles.outline_opacity")} - onSelected={setOutlineOpacity} + onSelected={(value) => updateSettings({ vlcOutlineOpacity: value })} /> - + updateSettings({ vlcIsBold: value })} + /> diff --git a/components/settings/VLCSubtitleSettings.tsx b/components/settings/VLCSubtitleSettings.tsx deleted file mode 100644 index 4596f781..00000000 --- a/components/settings/VLCSubtitleSettings.tsx +++ /dev/null @@ -1,131 +0,0 @@ -import { t } from "i18next"; -import { useEffect, useState } from "react"; -import { View } from "react-native"; -import { ListGroup } from "@/components/list/ListGroup"; -import { ListItem } from "@/components/list/ListItem"; -import { OUTLINE_THICKNESS, VLC_COLORS } from "@/constants/SubtitleConstants"; -import { storage } from "@/utils/mmkv"; - -export function VLCSubtitleSettings({ - className = "", -}: { - className?: string; -}) { - const [textColor, setTextColor] = useState( - storage.getString("vlc.textColor") || "White", - ); - const [backgroundColor, setBackgroundColor] = useState( - storage.getString("vlc.backgroundColor") || "Black", - ); - const [outlineColor, setOutlineColor] = useState( - storage.getString("vlc.outlineColor") || "Black", - ); - const [outlineThickness, setOutlineThickness] = useState( - storage.getString("vlc.outlineThickness") || "Normal", - ); - const [backgroundOpacity, setBackgroundOpacity] = useState( - storage.getNumber("vlc.backgroundOpacity") || 128, - ); - const [outlineOpacity, setOutlineOpacity] = useState( - storage.getNumber("vlc.outlineOpacity") || 255, - ); - const [isBold, setIsBold] = useState( - storage.getBoolean("vlc.isBold") || false, - ); - - useEffect(() => { - storage.set("vlc.textColor", textColor); - }, [textColor]); - - useEffect(() => { - storage.set("vlc.backgroundColor", backgroundColor); - }, [backgroundColor]); - - useEffect(() => { - storage.set("vlc.outlineColor", outlineColor); - }, [outlineColor]); - - useEffect(() => { - storage.set("vlc.outlineThickness", outlineThickness); - }, [outlineThickness]); - - useEffect(() => { - storage.set("vlc.backgroundOpacity", backgroundOpacity); - }, [backgroundOpacity]); - - useEffect(() => { - storage.set("vlc.outlineOpacity", outlineOpacity); - }, [outlineOpacity]); - - useEffect(() => { - storage.set("vlc.isBold", isBold); - }, [isBold]); - - return ( - - - { - const colors = Object.keys(VLC_COLORS); - const currentIndex = colors.indexOf(textColor); - const nextIndex = (currentIndex + 1) % colors.length; - setTextColor(colors[nextIndex]); - }} - /> - { - const colors = Object.keys(VLC_COLORS); - const currentIndex = colors.indexOf(backgroundColor); - const nextIndex = (currentIndex + 1) % colors.length; - setBackgroundColor(colors[nextIndex]); - }} - /> - { - const colors = Object.keys(VLC_COLORS); - const currentIndex = colors.indexOf(outlineColor); - const nextIndex = (currentIndex + 1) % colors.length; - setOutlineColor(colors[nextIndex]); - }} - /> - { - const thicknesses = Object.keys(OUTLINE_THICKNESS); - const currentIndex = thicknesses.indexOf(outlineThickness); - const nextIndex = (currentIndex + 1) % thicknesses.length; - setOutlineThickness(thicknesses[nextIndex]); - }} - /> - { - const newOpacity = (backgroundOpacity + 32) % 256; - setBackgroundOpacity(newOpacity); - }} - /> - { - const newOpacity = (outlineOpacity + 32) % 256; - setOutlineOpacity(newOpacity); - }} - /> - setIsBold(!isBold)} - /> - - - ); -} diff --git a/utils/atoms/settings.ts b/utils/atoms/settings.ts index c0780462..d7edeb20 100644 --- a/utils/atoms/settings.ts +++ b/utils/atoms/settings.ts @@ -168,6 +168,13 @@ export type Settings = { defaultPlayer: VideoPlayer; maxAutoPlayEpisodeCount: MaxAutoPlayEpisodeCount; autoPlayEpisodeCount: number; + vlcTextColor?: string; + vlcBackgroundColor?: string; + vlcOutlineColor?: string; + vlcOutlineThickness?: string; + vlcBackgroundOpacity?: number; + vlcOutlineOpacity?: number; + vlcIsBold?: boolean; // Gesture controls enableHorizontalSwipeSkip: boolean; enableLeftSideBrightnessSwipe: boolean; @@ -229,6 +236,13 @@ export const defaultValues: Settings = { defaultPlayer: VideoPlayer.VLC_3, // ios-only setting. does not matter what this is for android maxAutoPlayEpisodeCount: { key: "3", value: 3 }, autoPlayEpisodeCount: 0, + vlcTextColor: undefined, + vlcBackgroundColor: undefined, + vlcOutlineColor: undefined, + vlcOutlineThickness: undefined, + vlcBackgroundOpacity: undefined, + vlcOutlineOpacity: undefined, + vlcIsBold: undefined, // Gesture controls enableHorizontalSwipeSkip: true, enableLeftSideBrightnessSwipe: true, From 48cb0b70139de20906293ced4c51f561a62dbcde Mon Sep 17 00:00:00 2001 From: Uruk Date: Tue, 30 Sep 2025 12:17:38 +0200 Subject: [PATCH 23/29] fix: prevent permission errors when workflow runs from forks Adds fork detection to skip comment operations when running from external repositories, preventing 403 permission errors. Implements early exit when pull request or workflow run originates from a fork, and wraps comment operations in try-catch to handle remaining permission issues gracefully by logging build status instead. --- .github/workflows/artifact-comment.yml | 84 +++++++++++++++++--------- 1 file changed, 57 insertions(+), 27 deletions(-) diff --git a/.github/workflows/artifact-comment.yml b/.github/workflows/artifact-comment.yml index d032dd67..e6d902b1 100644 --- a/.github/workflows/artifact-comment.yml +++ b/.github/workflows/artifact-comment.yml @@ -29,6 +29,17 @@ jobs: uses: actions/github-script@v8 with: script: | + // Check if we're running from a fork + const isFromFork = context.payload.pull_request?.head?.repo?.full_name !== context.repo.owner + '/' + context.repo.repo; + const workflowFromFork = context.payload.workflow_run?.head_repository?.full_name !== context.repo.owner + '/' + context.repo.repo; + + if (isFromFork || workflowFromFork) { + console.log('đŸš« Workflow running from fork - skipping comment creation to avoid permission errors'); + console.log('Fork repository:', context.payload.pull_request?.head?.repo?.full_name || context.payload.workflow_run?.head_repository?.full_name); + console.log('Target repository:', context.repo.owner + '/' + context.repo.repo); + return; + } + // Handle repository_dispatch, pull_request, and manual dispatch events let pr; let targetCommitSha; @@ -403,34 +414,53 @@ jobs: commentBody += `*Auto-generated by [GitHub Actions](https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId})*`; commentBody += `\n`; - // Find existing bot comment to update - const { data: comments } = await github.rest.issues.listComments({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: pr.number - }); - - const botComment = comments.find(comment => - comment.user.type === 'Bot' && - comment.body.includes('') - ); - - if (botComment) { - // Update existing comment - await github.rest.issues.updateComment({ + // Try to find existing bot comment to update (with permission check) + try { + const { data: comments } = await github.rest.issues.listComments({ owner: context.repo.owner, repo: context.repo.repo, - comment_id: botComment.id, - body: commentBody + issue_number: pr.number }); - console.log(`✅ Updated comment ${botComment.id} on PR #${pr.number}`); - } else { - // Create new comment - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: pr.number, - body: commentBody - }); - console.log(`✅ Created new comment on PR #${pr.number}`); + + const botComment = comments.find(comment => + comment.user.type === 'Bot' && + comment.body.includes('') + ); + + if (botComment) { + // Update existing comment + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: botComment.id, + body: commentBody + }); + console.log(`✅ Updated comment ${botComment.id} on PR #${pr.number}`); + } else { + // Create new comment + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pr.number, + body: commentBody + }); + console.log(`✅ Created new comment on PR #${pr.number}`); + } + } catch (error) { + if (error.status === 403) { + console.log('đŸš« Permission denied - likely running from a fork. Skipping comment creation.'); + console.log('Error details:', error.message); + + // Log the build status instead of commenting + console.log('📊 Build Status Summary:'); + for (const target of buildTargets) { + const matchingStatus = buildStatuses[target.statusKey]; + if (matchingStatus) { + console.log(`- ${target.name}: ${matchingStatus.status}/${matchingStatus.conclusion || 'none'}`); + } + } + } else { + // Re-throw other errors + throw error; + } } From b372c353c0c97af81b2f1f523c0f7c338a5ae5ab Mon Sep 17 00:00:00 2001 From: Uruk Date: Tue, 30 Sep 2025 12:44:13 +0200 Subject: [PATCH 24/29] fix: improve fork detection logic in artifact comment workflow Enhances fork detection by implementing more precise repository comparison logic and adds comprehensive debugging output to troubleshoot permission issues. Changes null-safe comparisons to prevent false positives when repository information is undefined, ensuring the workflow only skips comment creation for actual cross-repository forks rather than same-repository scenarios. --- .github/workflows/artifact-comment.yml | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/.github/workflows/artifact-comment.yml b/.github/workflows/artifact-comment.yml index e6d902b1..949cb8a9 100644 --- a/.github/workflows/artifact-comment.yml +++ b/.github/workflows/artifact-comment.yml @@ -29,18 +29,30 @@ jobs: uses: actions/github-script@v8 with: script: | - // Check if we're running from a fork - const isFromFork = context.payload.pull_request?.head?.repo?.full_name !== context.repo.owner + '/' + context.repo.repo; - const workflowFromFork = context.payload.workflow_run?.head_repository?.full_name !== context.repo.owner + '/' + context.repo.repo; + // Check if we're running from a fork (more precise detection) + const targetRepo = context.repo.owner + '/' + context.repo.repo; + const prHeadRepo = context.payload.pull_request?.head?.repo?.full_name; + const workflowHeadRepo = context.payload.workflow_run?.head_repository?.full_name; + + // For debugging + console.log('🔍 Repository detection:'); + console.log('- Target repository:', targetRepo); + console.log('- PR head repository:', prHeadRepo || 'N/A'); + console.log('- Workflow head repository:', workflowHeadRepo || 'N/A'); + console.log('- Event name:', context.eventName); + + // Only skip if it's actually a different repository (fork) + const isFromFork = prHeadRepo && prHeadRepo !== targetRepo; + const workflowFromFork = workflowHeadRepo && workflowHeadRepo !== targetRepo; if (isFromFork || workflowFromFork) { console.log('đŸš« Workflow running from fork - skipping comment creation to avoid permission errors'); - console.log('Fork repository:', context.payload.pull_request?.head?.repo?.full_name || context.payload.workflow_run?.head_repository?.full_name); - console.log('Target repository:', context.repo.owner + '/' + context.repo.repo); + console.log('Fork repository:', prHeadRepo || workflowHeadRepo); + console.log('Target repository:', targetRepo); return; } - // Handle repository_dispatch, pull_request, and manual dispatch events + console.log('✅ Same repository - proceeding with comment creation'); // Handle repository_dispatch, pull_request, and manual dispatch events let pr; let targetCommitSha; From 4a28352b53e4f3e1764542ff37d709ef56e6ad06 Mon Sep 17 00:00:00 2001 From: Fredrik Burmester Date: Wed, 1 Oct 2025 08:32:34 +0200 Subject: [PATCH 25/29] chore: remove log --- components/settings/HomeIndex.tsx | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/components/settings/HomeIndex.tsx b/components/settings/HomeIndex.tsx index e1e08d09..a212f0a8 100644 --- a/components/settings/HomeIndex.tsx +++ b/components/settings/HomeIndex.tsx @@ -460,12 +460,7 @@ export const HomeIndex = () => { style={{ marginTop: Platform.isTV ? 0 : -100 }} contentContainerStyle={{ paddingTop: Platform.isTV ? 0 : 100 }} > - { - console.log(`Now viewing carousel item ${index}`); - }} - /> + Date: Wed, 1 Oct 2025 09:12:34 +0200 Subject: [PATCH 26/29] chore: version --- app.json | 4 ++-- eas.json | 6 +++--- providers/JellyfinProvider.tsx | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app.json b/app.json index f77e82cb..89a1274c 100644 --- a/app.json +++ b/app.json @@ -2,7 +2,7 @@ "expo": { "name": "Streamyfin", "slug": "streamyfin", - "version": "0.38.0", + "version": "0.39.0", "orientation": "default", "icon": "./assets/images/icon.png", "scheme": "streamyfin", @@ -37,7 +37,7 @@ }, "android": { "jsEngine": "hermes", - "versionCode": 70, + "versionCode": 71, "adaptiveIcon": { "foregroundImage": "./assets/images/icon-android-plain.png", "monochromeImage": "./assets/images/icon-android-themed.png", diff --git a/eas.json b/eas.json index f7918986..a6c4cf52 100644 --- a/eas.json +++ b/eas.json @@ -45,14 +45,14 @@ }, "production": { "environment": "production", - "channel": "0.38.0", + "channel": "0.39.0", "android": { "image": "latest" } }, "production-apk": { "environment": "production", - "channel": "0.38.0", + "channel": "0.39.0", "android": { "buildType": "apk", "image": "latest" @@ -60,7 +60,7 @@ }, "production-apk-tv": { "environment": "production", - "channel": "0.38.0", + "channel": "0.39.0", "android": { "buildType": "apk", "image": "latest" diff --git a/providers/JellyfinProvider.tsx b/providers/JellyfinProvider.tsx index 3c07dd48..e9b4feae 100644 --- a/providers/JellyfinProvider.tsx +++ b/providers/JellyfinProvider.tsx @@ -64,7 +64,7 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({ setJellyfin( () => new Jellyfin({ - clientInfo: { name: "Streamyfin", version: "0.38.0" }, + clientInfo: { name: "Streamyfin", version: "0.39.0" }, deviceInfo: { name: deviceName, id, @@ -87,7 +87,7 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({ return { authorization: `MediaBrowser Client="Streamyfin", Device=${ Platform.OS === "android" ? "Android" : "iOS" - }, DeviceId="${deviceId}", Version="0.38.0"`, + }, DeviceId="${deviceId}", Version="0.39.0"`, }; }, [deviceId]); From 2f2099e243e00dfd0e6434224149aa1bd3647db9 Mon Sep 17 00:00:00 2001 From: Fredrik Burmester Date: Wed, 1 Oct 2025 09:12:49 +0200 Subject: [PATCH 27/29] fix: android header not visible --- app/(auth)/(tabs)/(libraries)/_layout.tsx | 6 +++--- components/stacks/NestedTabPageStack.tsx | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/app/(auth)/(tabs)/(libraries)/_layout.tsx b/app/(auth)/(tabs)/(libraries)/_layout.tsx index 89a3e847..8c7fb259 100644 --- a/app/(auth)/(tabs)/(libraries)/_layout.tsx +++ b/app/(auth)/(tabs)/(libraries)/_layout.tsx @@ -24,7 +24,7 @@ export default function IndexLayout() { headerShown: !Platform.isTV, headerTitle: t("tabs.library"), headerBlurEffect: "none", - headerTransparent: true, + headerTransparent: Platform.OS === "ios", headerShadowVisible: false, headerRight: () => !pluginSettings?.libraryOptions?.locked && @@ -48,7 +48,7 @@ export default function IndexLayout() { title: "", headerShown: !Platform.isTV, headerBlurEffect: "none", - headerTransparent: true, + headerTransparent: Platform.OS === "ios", headerShadowVisible: false, }} /> @@ -61,7 +61,7 @@ export default function IndexLayout() { title: "", headerShown: !Platform.isTV, headerBlurEffect: "none", - headerTransparent: true, + headerTransparent: Platform.OS === "ios", headerShadowVisible: false, }} /> diff --git a/components/stacks/NestedTabPageStack.tsx b/components/stacks/NestedTabPageStack.tsx index ec4ba1e8..87f92db6 100644 --- a/components/stacks/NestedTabPageStack.tsx +++ b/components/stacks/NestedTabPageStack.tsx @@ -1,5 +1,6 @@ import type { ParamListBase, RouteProp } from "@react-navigation/native"; import type { NativeStackNavigationOptions } from "@react-navigation/native-stack"; +import { Platform } from "react-native"; import { HeaderBackButton } from "../common/HeaderBackButton"; type ICommonScreenOptions = @@ -12,7 +13,7 @@ type ICommonScreenOptions = export const commonScreenOptions: ICommonScreenOptions = { title: "", headerShown: true, - headerTransparent: true, + headerTransparent: Platform.OS === "ios", headerShadowVisible: false, headerBlurEffect: "none", headerLeft: () => , From d7b4e01aa582d0198573e9924275edb7eb6ec7db Mon Sep 17 00:00:00 2001 From: Simon Eklundh Date: Wed, 1 Oct 2025 21:24:03 +0200 Subject: [PATCH 28/29] feat: add crowdin integration to streamyfin (#1103) Co-authored-by: Fredrik Burmester --- .github/workflows/crowdin.yml | 34 ++++++++++++++++++++++++++++++++++ crowdin.yml | 12 ++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 .github/workflows/crowdin.yml create mode 100644 crowdin.yml diff --git a/.github/workflows/crowdin.yml b/.github/workflows/crowdin.yml new file mode 100644 index 00000000..b181d42a --- /dev/null +++ b/.github/workflows/crowdin.yml @@ -0,0 +1,34 @@ +name: Crowdin Action + +on: + push: + branches: [ main ] + +jobs: + synchronize-with-crowdin: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: crowdin action + uses: crowdin/github-action@v2 + with: + upload_sources: true + upload_translations: true + download_translations: true + localization_branch_name: l10n_crowdin_translations + create_pull_request: true + pull_request_title: 'feat: New Crowdin Translations' + pull_request_body: 'New Crowdin translations by [Crowdin GH Action](https://github.com/crowdin/github-action)' + pull_request_base_branch_name: 'develop' + env: + # A classic GitHub Personal Access Token with the 'repo' scope selected (the user should have write access to the repository). + GITHUB_TOKEN: ${{ secrets.CROWDIN_GITHUB_TOKEN }} + + # A numeric ID, found at https://crowdin.com/project//tools/api + CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }} + + # Visit https://crowdin.com/settings#api-key to create this token + CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} \ No newline at end of file diff --git a/crowdin.yml b/crowdin.yml new file mode 100644 index 00000000..38b86bcd --- /dev/null +++ b/crowdin.yml @@ -0,0 +1,12 @@ +"project_id_env": "CROWDIN_PROJECT_ID" +"api_token_env": "CROWDIN_PERSONAL_TOKEN" +"base_path": "." + +"preserve_hierarchy": true + +"files": [ + { + "source": "translations/en.json", + "translation": "translations/%two_letters_code%.json" + } +] \ No newline at end of file From 08c7382191414435e3ef9635b8f862ee8beed81d Mon Sep 17 00:00:00 2001 From: SuxAsLux <47444874+SuxAsLux@users.noreply.github.com> Date: Wed, 1 Oct 2025 22:18:54 +0200 Subject: [PATCH 29/29] feat(lang): Add full Hungarian translation (#1090) Co-authored-by: SuxAsLux Co-authored-by: Gauvain <68083474+Gauvino@users.noreply.github.com> Co-authored-by: Simon Eklundh --- translations/hu.json | 464 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 464 insertions(+) create mode 100644 translations/hu.json diff --git a/translations/hu.json b/translations/hu.json new file mode 100644 index 00000000..9306d339 --- /dev/null +++ b/translations/hu.json @@ -0,0 +1,464 @@ +{ + "login": { + "username_required": "A felhasznĂĄlĂłnĂ©v megadĂĄsa kötelezƑ", + "error_title": "Hiba", + "login_title": "BejelentkezĂ©s", + "login_to_title": "BejelentkezĂ©s ide", + "username_placeholder": "FelhasznĂĄlĂłnĂ©v", + "password_placeholder": "JelszĂł", + "login_button": "BejelentkezĂ©s", + "quick_connect": "GyorscsatlakozĂĄs", + "enter_code_to_login": "Írd be a {{code}} kĂłdot a bejelentkezĂ©shez", + "failed_to_initiate_quick_connect": "A GyorscsatlakozĂĄs kezdemĂ©nyezĂ©se sikertelen.", + "got_it": "Értettem", + "connection_failed": "KapcsolĂłdĂĄs Sikertelen", + "could_not_connect_to_server": "Nem sikerĂŒlt csatlakozni a szerverhez. KĂ©rjĂŒk, ellenƑrizd az URL-t Ă©s a hĂĄlĂłzati kapcsolatot.", + "an_unexpected_error_occured": "VĂĄratlan Hiba TörtĂ©nt", + "change_server": "SzervervĂĄltĂĄs", + "invalid_username_or_password": "ÉrvĂ©nytelen FelhasznĂĄlĂłnĂ©v vagy JelszĂł", + "user_does_not_have_permission_to_log_in": "A felhasznĂĄlĂłnak nincs jogosultsĂĄga a bejelentkezĂ©shez", + "server_is_taking_too_long_to_respond_try_again_later": "A szerver tĂșl sokĂĄig vĂĄlaszol, prĂłbĂĄld Ășjra kĂ©sƑbb", + "server_received_too_many_requests_try_again_later": "A szerver tĂșl sok kĂ©rĂ©st kapott, prĂłbĂĄld Ășjra kĂ©sƑbb.", + "there_is_a_server_error": "Szerverhiba törtĂ©nt", + "an_unexpected_error_occured_did_you_enter_the_correct_url": "VĂĄratlan hiba törtĂ©nt. Helyesen adtad meg a szerver URL-jĂ©t?", + "too_old_server_text": "Nem TĂĄmogatott Jellyfin-szerver", + "too_old_server_description": "FrissĂ­tsd a Jellyfint a legĂșjabb verziĂłra" + }, + "server": { + "enter_url_to_jellyfin_server": "Add meg a Jellyfin szerver URL-jĂ©t", + "server_url_placeholder": "http(s)://a-te-szervered.hu", + "connect_button": "CsatlakozĂĄs", + "previous_servers": "ElƑzƑ Szerverek", + "clear_button": "TörlĂ©s", + "search_for_local_servers": "Helyi Szerverek KeresĂ©se", + "searching": "KeresĂ©s...", + "servers": "Szerverek" + }, + "home": { + "checking_server_connection": "Szerverkapcsolat ellenƑrzĂ©se...", + "no_internet": "Nincs Internet", + "no_items": "Nincsenek elemek", + "no_internet_message": "Semmi gond, tovĂĄbbra is nĂ©zheted\na letöltött tartalmakat.", + "checking_server_connection_message": "Kapcsolat ellenƑrzĂ©se a szerverrel", + "go_to_downloads": "UgrĂĄs a LetöltĂ©sekhez", + "retry": "Újra", + "server_unreachable": "Szerver ElĂ©rhetetlen", + "server_unreachable_message": "Nem sikerĂŒlt elĂ©rni a szervert.\nKĂ©rjĂŒk, ellenƑrizd a hĂĄlĂłzati kapcsolatot.", + "oops": "HoppĂĄ!", + "error_message": "Valami nem stimmel.\nKĂ©rjĂŒk, jelentkezz ki, majd Ășjra be.", + "continue_watching": "NĂ©zd TovĂĄbb", + "next_up": "KövetkezƑ", + "recently_added_in": "Új a(z) {{libraryName}} könyvtĂĄrban", + "suggested_movies": "Javasolt Filmek", + "suggested_episodes": "Javasolt EpizĂłdok", + "intro": { + "welcome_to_streamyfin": "ÜdvözöljĂŒk a Streamyfinben", + "a_free_and_open_source_client_for_jellyfin": "Egy Ingyenes Ă©s NyĂ­lt ForrĂĄskĂłdĂș Jellyfin Kliens.", + "features_title": "FunkciĂłk", + "features_description": "A Streamyfin szĂĄmos funkciĂłval rendelkezik Ă©s sokfĂ©le szoftverrel integrĂĄlhatĂł, melyeket a beĂĄllĂ­tĂĄsok menĂŒben talĂĄlhatsz:", + "jellyseerr_feature_description": "Csatlakozz a Jellyseerrhez Ă©s kĂ©rj filmeket közvetlenĂŒl az alkalmazĂĄsbĂłl.", + "downloads_feature_title": "LetöltĂ©sek", + "downloads_feature_description": "Töltsd le a filmeket Ă©s sorozatokat az offline megtekintĂ©shez. HasznĂĄlhatod az alapĂ©rtelmezett mĂłdszert, vagy telepĂ­theted az 'optimise server'-t a hĂĄttĂ©rben törtĂ©nƑ letöltĂ©shez.", + "chromecast_feature_description": "JĂĄtszd le a filmeket Ă©s sorozatokat a Chromecast eszközeiden.", + "centralised_settings_plugin_title": "KözpontosĂ­tott BeĂĄllĂ­tĂĄsok BƑvĂ­tmĂ©ny", + "centralised_settings_plugin_description": "KonfigurĂĄld a beĂĄllĂ­tĂĄsaidat központilag a Jellyfin szerveren. Minden felhasznĂĄlĂłi kliensbeĂĄllĂ­tĂĄs automatikusan szinkronizĂĄlĂłdik.", + "done_button": "KĂ©sz", + "go_to_settings_button": "UgrĂĄs a BeĂĄllĂ­tĂĄsokhoz", + "read_more": "BƑvebben" + }, + "settings": { + "settings_title": "BeĂĄllĂ­tĂĄsok", + "log_out_button": "KijelentkezĂ©s", + "user_info": { + "user_info_title": "FelhasznĂĄlĂłi InformĂĄciĂłk", + "user": "FelhasznĂĄlĂł", + "server": "Szerver", + "token": "Token", + "app_version": "AlkalmazĂĄsverziĂł" + }, + "quick_connect": { + "quick_connect_title": "GyorscsatlakozĂĄs", + "authorize_button": "GyorscsatlakozĂĄs EngedĂ©lyezĂ©se", + "enter_the_quick_connect_code": "Add meg a gyors csatlakozĂĄsi kĂłdot...", + "success": "Siker", + "quick_connect_autorized": "GyorscsatlakozĂĄs EngedĂ©lyezve", + "error": "Hiba", + "invalid_code": "ÉrvĂ©nytelen KĂłd", + "authorize": "EngedĂ©lyezĂ©s" + }, + "media_controls": { + "media_controls_title": "MĂ©diavezĂ©rlĂ©s", + "forward_skip_length": "ElƑre UgrĂĄs Hossza", + "rewind_length": "VisszatekerĂ©s Hossza", + "seconds_unit": "mp" + }, + "gesture_controls": { + "gesture_controls_title": "GesztusvezĂ©rlĂ©s", + "horizontal_swipe_skip": "VĂ­zszintes HĂșzĂĄs UgrĂĄshoz", + "horizontal_swipe_skip_description": "Ha a vezĂ©rlƑk el vannak rejtve, hĂșzd balra vagy jobbra az ugrĂĄshoz.", + "left_side_brightness": "FĂ©nyerƑ a Bal Oldalon", + "left_side_brightness_description": "HĂșzd felfelĂ© vagy lefelĂ© a bal oldalon a fĂ©nyerƑ ĂĄllĂ­tĂĄsĂĄhoz", + "right_side_volume": "FĂ©nyerƑ a Jobb Oldalon", + "right_side_volume_description": "HĂșzd felfelĂ© vagy lefelĂ© a jobb oldalon a hangerƑ ĂĄllĂ­tĂĄsĂĄhoz" + }, + "audio": { + "audio_title": "Hang", + "set_audio_track": "HangsĂĄv BeĂĄllĂ­tĂĄsa az ElƑzƑ ElembƑl", + "audio_language": "HangsĂĄv Nyelve", + "audio_hint": "VĂĄlassz AlapĂ©rtelmezett HangsĂĄvnyelvet.", + "none": "Nincs", + "language": "Nyelv" + }, + "subtitles": { + "subtitle_title": "Feliratok", + "subtitle_language": "Felirat Nyelve", + "subtitle_mode": "Felirat MĂłdja", + "set_subtitle_track": "FeliratsĂĄv BeĂĄllĂ­tĂĄsa az ElƑzƑ ElembƑl", + "subtitle_size": "Felirat MĂ©rete", + "subtitle_hint": "FeliratbeĂĄllĂ­tĂĄsok MegadĂĄsa", + "none": "Nincs", + "language": "Nyelv", + "loading": "BetöltĂ©s", + "modes": { + "Default": "AlapĂ©rtelmezett", + "Smart": "Intelligens", + "Always": "Mindig", + "None": "Nincs", + "OnlyForced": "Csak KĂ©nyszerĂ­tett" + } + }, + "other": { + "other_title": "EgyĂ©b", + "follow_device_orientation": "Automatikus ForgatĂĄs", + "video_orientation": "VideĂł TĂĄjolĂĄs", + "orientation": "TĂĄjolĂĄs", + "orientations": { + "DEFAULT": "AlapĂ©rtelmezett", + "ALL": "Összes", + "PORTRAIT": "ÁllĂł", + "PORTRAIT_UP": "ÁllĂł FelfelĂ©", + "PORTRAIT_DOWN": "ÁllĂł LefelĂ©", + "LANDSCAPE": "FekvƑ", + "LANDSCAPE_LEFT": "FekvƑ Balra", + "LANDSCAPE_RIGHT": "FekvƑ Jobbra", + "OTHER": "EgyĂ©b", + "UNKNOWN": "Ismeretlen" + }, + "safe_area_in_controls": "BiztonsĂĄgi SĂĄv a VezĂ©rlƑkben", + "video_player": "VideĂłlejĂĄtszĂł", + "video_players": { + "VLC_3": "VLC 3", + "VLC_4": "VLC 4 (KĂ­sĂ©rleti + PiP)" + }, + "show_custom_menu_links": "EgyĂ©ni MenĂŒlinkek MegjelenĂ­tĂ©se", + "hide_libraries": "KönyvtĂĄrak ElrejtĂ©se", + "select_liraries_you_want_to_hide": "VĂĄlaszd ki azokat a könyvtĂĄrakat, amelyeket el szeretnĂ©l rejteni a KönyvtĂĄr fĂŒlön Ă©s a kezdƑlapon.", + "disable_haptic_feedback": "Haptikus VisszajelzĂ©s LetiltĂĄsa", + "default_quality": "AlapĂ©rtelmezett MinƑsĂ©g", + "max_auto_play_episode_count": "Max. Auto. EpizĂłdlejĂĄtszĂĄs", + "disabled": "Letiltva" + }, + "downloads": { + "downloads_title": "LetöltĂ©sek", + "remux_max_download": "Remux MaximĂĄlis LetöltĂ©s" + }, + "plugins": { + "plugins_title": "BƑvĂ­tmĂ©nyek", + "jellyseerr": { + "jellyseerr_warning": "Ez az integrĂĄciĂł mĂ©g korai stĂĄdiumban van. SzĂĄmĂ­ts a vĂĄltozĂĄsokra.", + "server_url": "Szerver URL", + "server_url_hint": "PĂ©lda: http(s)://a-te-szolgĂĄltatĂłd.url\n(adj meg portot, ha szĂŒksĂ©ges)", + "server_url_placeholder": "Jellyseerr URL...", + "password": "JelszĂł", + "password_placeholder": "Add meg a {{username}} Jellyfin felhasznĂĄlĂł jelszavĂĄt", + "login_button": "BejelentkezĂ©s", + "total_media_requests": "Összes MĂ©dia KĂ©rĂ©s", + "movie_quota_limit": "Film KvĂłta Limit", + "movie_quota_days": "Film KvĂłta Napok", + "tv_quota_limit": "Sorozat KvĂłta Limit", + "tv_quota_days": "Sorozat KvĂłta Napok", + "reset_jellyseerr_config_button": "Jellyseerr BeĂĄllĂ­tĂĄsok VisszaĂĄllĂ­tĂĄsa", + "unlimited": "KorlĂĄtlan", + "plus_n_more": "+{{n}} TovĂĄbbi", + "order_by": { + "DEFAULT": "AlapĂ©rtelmezett", + "VOTE_COUNT_AND_AVERAGE": "Szavazatok SzĂĄma Ă©s Átlag", + "POPULARITY": "NĂ©pszerƱsĂ©g" + } + }, + "marlin_search": { + "enable_marlin_search": "Marlin KeresĂ©s EngedĂ©lyezĂ©se", + "url": "URL", + "server_url_placeholder": "http(s)://domain.org:port", + "marlin_search_hint": "Add meg a Marlin szerver URL-jĂ©t. Az URL-nek tartalmaznia kell a http vagy https-t, Ă©s opcionĂĄlisan a portot.", + "read_more_about_marlin": "Tudj Meg Többet a MarlinrĂłl", + "save_button": "MentĂ©s", + "toasts": { + "saved": "Mentve" + } + } + }, + "storage": { + "storage_title": "TĂĄrhely", + "app_usage": "AlkalmazĂĄs {{usedSpace}}%", + "device_usage": "Eszköz {{availableSpace}}%", + "size_used": "{{used}} / {{total}} HasznĂĄlatban", + "delete_all_downloaded_files": "Minden Letöltött FĂĄjl TörlĂ©se" + }, + "intro": { + "show_intro": "BemutatĂł MegjelenĂ­tĂ©se", + "reset_intro": "BemutatĂł VisszaĂĄllĂ­tĂĄsa" + }, + "logs": { + "logs_title": "NaplĂłk", + "export_logs": "NaplĂłk ExportĂĄlĂĄsa", + "click_for_more_info": "Kattints a RĂ©szletekĂ©rt", + "level": "Szint", + "no_logs_available": "Nincsenek NaplĂłk", + "delete_all_logs": "Összes NaplĂł TörlĂ©se" + }, + "languages": { + "title": "Nyelvek", + "app_language": "AlkalmazĂĄs Nyelve", + "system": "Rendszer" + }, + "toasts": { + "error_deleting_files": "Hiba a FĂĄjlok TörlĂ©sekor" + } + }, + "sessions": { + "title": "Munkamenetek", + "no_active_sessions": "Nincsenek AktĂ­v Munkamenetek" + }, + "downloads": { + "downloads_title": "LetöltĂ©sek", + "tvseries": "Sorozatok", + "movies": "Filmek", + "queue": "Sor", + "queue_hint": "A sor Ă©s a letöltĂ©sek az alkalmazĂĄs ĂșjraindĂ­tĂĄsakor elvesznek", + "no_items_in_queue": "Nincs Elem a Sorban", + "no_downloaded_items": "Nincsenek Letöltött Elemek", + "delete_all_movies_button": "Összes Film TörlĂ©se", + "delete_all_tvseries_button": "Összes Sorozat TörlĂ©se", + "delete_all_button": "Összes TörlĂ©se", + "active_download": "AktĂ­v LetöltĂ©s", + "no_active_downloads": "Nincs AktĂ­v LetöltĂ©s", + "active_downloads": "AktĂ­v LetöltĂ©sek", + "new_app_version_requires_re_download": "Az Új AlkalmazĂĄsverziĂł Újra LetöltĂ©st IgĂ©nyel", + "new_app_version_requires_re_download_description": "Az Ășj frissĂ­tĂ©shez az összes tartalmat Ășjra le kell tölteni. KĂ©rjĂŒk, töröld az összes letöltött tartalmat, majd prĂłbĂĄld Ășjra.", + "back": "Vissza", + "delete": "TörlĂ©s", + "something_went_wrong": "Hiba TörtĂ©nt", + "could_not_get_stream_url_from_jellyfin": "Nem sikerĂŒlt lekĂ©rni a stream URL-t a JellyfinbƑl", + "eta": "VĂĄrhatĂł IdƑ: {{eta}}", + "toasts": { + "you_are_not_allowed_to_download_files": "Nem engedĂ©lyezett a fĂĄjlok letöltĂ©se.", + "deleted_all_movies_successfully": "Az Összes Film Sikeresen Törölve!", + "failed_to_delete_all_movies": "Nem SikerĂŒlt Törölni Az Összes Filmet", + "deleted_all_tvseries_successfully": "Az Összes Sorozat Sikeresen Törölve!", + "failed_to_delete_all_tvseries": "Nem SikerĂŒlt Törölni Az Összes Sorozatot", + "download_deleted": "LetöltĂ©s Törölve", + "could_not_delete_download": "Nem SikerĂŒlt Törölni a LetöltĂ©st", + "download_paused": "LetöltĂ©s SzĂŒneteltetve", + "could_not_pause_download": "Nem SikerĂŒlt SzĂŒneteltetni a LetöltĂ©st", + "download_resumed": "LetöltĂ©s Folytatva", + "could_not_resume_download": "Nem SikerĂŒlt Folytatni a LetöltĂ©st", + "download_completed": "LetöltĂ©s Befejezve", + "download_failed_for_item": "A(z) {{item}} letöltĂ©se sikertelen - {{error}}", + "download_completed_for_item": "A(z) {{item}} letöltĂ©se befejezve", + "all_files_folders_and_jobs_deleted_successfully": "Minden fĂĄjl, mappa Ă©s feladat sikeresen törölve", + "go_to_downloads": "UgrĂĄs a LetöltĂ©sekhez" + } + } + }, + "search": { + "search": "KeresĂ©s...", + "x_items": "{{count}} Elem", + "library": "KönyvtĂĄr", + "discover": "FelfedezĂ©s", + "no_results": "Nincs EredmĂ©ny", + "no_results_found_for": "Nincs EredmĂ©ny a KeresĂ©shez", + "movies": "Filmek", + "series": "Sorozatok", + "episodes": "EpizĂłdok", + "collections": "GyƱjtemĂ©nyek", + "actors": "SzĂ­nĂ©szek", + "request_movies": "Filmek KĂ©rĂ©se", + "request_series": "Sorozatok KĂ©rĂ©se", + "recently_added": "LegutĂłbb HozzĂĄadva", + "recent_requests": "LegutĂłbbi KĂ©rĂ©sek", + "plex_watchlist": "Plex Watchlist", + "trending": "NĂ©pszerƱ", + "popular_movies": "NĂ©pszerƱ Filmek", + "movie_genres": "Film MƱfajok", + "upcoming_movies": "Hamarosan MegjelenƑ Filmek", + "studios": "StĂșdiĂłk", + "popular_tv": "NĂ©pszerƱ Sorozatok", + "tv_genres": "Sorozat MƱfajok", + "upcoming_tv": "Hamarosan MegjelenƑ Sorozatok", + "networks": "CsatornĂĄk", + "tmdb_movie_keyword": "TMDB Film KulcsszĂł", + "tmdb_movie_genre": "TMDB Film MƱfaj", + "tmdb_tv_keyword": "TMDB Sorozat KulcsszĂł", + "tmdb_tv_genre": "TMDB Sorozat MƱfaj", + "tmdb_search": "TMDB KeresĂ©s", + "tmdb_studio": "TMDB StĂșdiĂł", + "tmdb_network": "TMDB Csatorna", + "tmdb_movie_streaming_services": "TMDB Film Streaming SzolgĂĄltatĂĄsok", + "tmdb_tv_streaming_services": "TMDB Sorozat Streaming SzolgĂĄltatĂĄsok" + }, + "library": { + "no_results": "Nincs EredmĂ©ny", + "no_libraries_found": "Nem TalĂĄlhatĂł KönyvtĂĄr", + "item_types": { + "movies": "Filmek", + "series": "Sorozatok", + "boxsets": "GyƱjtemĂ©nyek", + "items": "Elemek" + }, + "options": { + "display": "MegjelenĂ­tĂ©s", + "row": "Sor", + "list": "Lista", + "image_style": "KĂ©p StĂ­lusa", + "poster": "Poszter", + "cover": "BorĂ­tĂł", + "show_titles": "CĂ­mek MegjelenĂ­tĂ©se", + "show_stats": "StatisztikĂĄk MegjelenĂ­tĂ©se" + }, + "filters": { + "genres": "MƱfajok", + "years": "Évek", + "sort_by": "RendezĂ©s", + "sort_order": "RendezĂ©s IrĂĄnya", + "tags": "CĂ­mkĂ©k" + } + }, + "favorites": { + "series": "Sorozatok", + "movies": "Filmek", + "episodes": "EpizĂłdok", + "videos": "VideĂłk", + "boxsets": "GyƱjtemĂ©nyek", + "playlists": "LejĂĄtszĂĄsi ListĂĄk", + "noDataTitle": "MĂ©g Nincsenek Kedvencek", + "noData": "Jelölj meg elemeket kedvenckĂ©nt, hogy itt gyorsan elĂ©rd Ƒket." + }, + "custom_links": { + "no_links": "Nincsenek Linkek" + }, + "player": { + "error": "Hiba", + "failed_to_get_stream_url": "Nem sikerĂŒlt lekĂ©rni a stream URL-t", + "an_error_occured_while_playing_the_video": "Hiba törtĂ©nt a videĂł lejĂĄtszĂĄsa közben. EllenƑrizd a naplĂłkat a beĂĄllĂ­tĂĄsokban.", + "client_error": "Kliens Hiba", + "could_not_create_stream_for_chromecast": "A Chromecast stream lĂ©trehozĂĄsa sikertelen volt", + "message_from_server": "Üzenet a szervertƑl: {{message}}", + "next_episode": "KövetkezƑ EpizĂłd", + "refresh_tracks": "SĂĄvok FrissĂ­tĂ©se", + "audio_tracks": "HangsĂĄvok:", + "playback_state": "LejĂĄtszĂĄs Állapota:", + "index": "Index:", + "continue_watching": "FolytatĂĄs", + "go_back": "Vissza" + }, + "item_card": { + "next_up": "KövetkezƑ", + "no_items_to_display": "Nincs MegjelenĂ­thetƑ Elem", + "cast_and_crew": "SzereplƑk & StĂĄb", + "series": "Sorozat", + "seasons": "Évadok", + "season": "Évad", + "no_episodes_for_this_season": "Ehhez az Ă©vadhoz nincs epizĂłd", + "overview": "ÁttekintĂ©s", + "more_with": "TovĂĄbbi {{name}} AlkotĂĄsok", + "similar_items": "HasonlĂł Elemek", + "no_similar_items_found": "Nincs HasonlĂł Elem", + "video": "VideĂł", + "more_details": "TovĂĄbbi RĂ©szletek", + "quality": "MinƑsĂ©g", + "audio": "Hang", + "subtitles": "Felirat", + "show_more": "Több MegjelenĂ­tĂ©se", + "show_less": "Kevesebb MegjelenĂ­tĂ©se", + "appeared_in": "Megjelent:", + "could_not_load_item": "Nem SikerĂŒlt Betölteni az Elemet", + "none": "Nincs", + "download": { + "download_season": "Évad LetöltĂ©se", + "download_series": "Sorozat LetöltĂ©se", + "download_episode": "EpizĂłd LetöltĂ©se", + "download_movie": "Film LetöltĂ©se", + "download_x_item": "{{item_count}} Elem LetöltĂ©se", + "download_unwatched_only": "Csak Nem Megtekintett", + "download_button": "LetöltĂ©s" + } + }, + "live_tv": { + "next": "KövetkezƑ", + "previous": "ElƑzƑ", + "coming_soon": "Hamarosan", + "on_now": "Most MƱsoron", + "shows": "Sorozatok", + "movies": "Filmek", + "sports": "Sport", + "for_kids": "Gyerekeknek", + "news": "HĂ­rek" + }, + "jellyseerr": { + "confirm": "MegerƑsĂ­tĂ©s", + "cancel": "MĂ©gse", + "yes": "Igen", + "whats_wrong": "Mi a ProblĂ©ma?", + "issue_type": "ProblĂ©ma TĂ­pusa", + "select_an_issue": "VĂĄlassz ProblĂ©mĂĄt", + "types": "TĂ­pusok", + "describe_the_issue": "(OpcionĂĄlis) Fejtsd ki a problĂ©mĂĄt...", + "submit_button": "BekĂŒldĂ©s", + "report_issue_button": "ProblĂ©ma JelentĂ©se", + "request_button": "KĂ©rĂ©s", + "are_you_sure_you_want_to_request_all_seasons": "Biztosan az összes Ă©vadot kĂ©red?", + "failed_to_login": "Sikertelen BejelentkezĂ©s", + "cast": "SzereplƑk", + "details": "RĂ©szletek", + "status": "Állapot", + "original_title": "Eredeti CĂ­m", + "series_type": "Sorozat TĂ­pusa", + "release_dates": "MegjelenĂ©si DĂĄtumok", + "first_air_date": "ElsƑ VetĂ­tĂ©s DĂĄtuma", + "next_air_date": "KövetkezƑ AdĂĄs DĂĄtuma", + "revenue": "BevĂ©tel", + "budget": "KöltsĂ©gvetĂ©s", + "original_language": "Eredeti Nyelv", + "production_country": "GyĂĄrtĂĄsi OrszĂĄg", + "studios": "StĂșdiĂłk", + "network": "Csatorna", + "currently_streaming_on": "Jelenleg ElĂ©rhetƑ:", + "advanced": "HaladĂł", + "request_as": "KĂ©rĂ©s MĂĄs FelhasznĂĄlĂłkĂ©nt", + "tags": "CĂ­mkĂ©k", + "quality_profile": "MinƑsĂ©gi Profil", + "root_folder": "GyökĂ©rmappa", + "season_all": "Évad (Összes)", + "season_number": "Évad {{season_number}}", + "number_episodes": "{{episode_number}} EpizĂłd", + "born": "SzĂŒletett", + "appearances": "MegjelenĂ©sek", + "toasts": { + "jellyseer_does_not_meet_requirements": "A Jellyseerr szerver nem felel meg a minimum verziĂłkövetelmĂ©nyeknek! KĂ©rlek frissĂ­tsd legalĂĄbb 2.0.0-ra.", + "jellyseerr_test_failed": "A Jellyseerr teszt sikertelen. PrĂłbĂĄld Ășjra.", + "failed_to_test_jellyseerr_server_url": "Nem sikerĂŒlt tesztelni a Jellyseerr szerver URL-jĂ©t", + "issue_submitted": "ProblĂ©ma BekĂŒldve!", + "requested_item": "{{item}} KĂ©rĂ©se Sikeres!", + "you_dont_have_permission_to_request": "Nincs jogosultsĂĄgod a kĂ©rĂ©shez!", + "something_went_wrong_requesting_media": "Hiba törtĂ©nt a mĂ©dia kĂ©rĂ©s közben!" + } + }, + "tabs": { + "home": "KezdƑlap", + "search": "KeresĂ©s", + "library": "KönyvtĂĄr", + "custom_links": "EgyĂ©ni Linkek", + "favorites": "Kedvencek" + } +}