Rebrand as Smithflix and add AI picks row

- Smithflix name, channel posters, splash screens and logos
- 'Because you watched ...' row (the user's AI playlist) below Continue Watching
- Build workflow publishing a sideloadable smithflix-roku.zip

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011TdkZRTCV92L7gbQX2Z5Ay
This commit is contained in:
Claude 2026-10-10 19:29:42 -04:00
parent 72cfe985d4
commit 84d2849fcf
No known key found for this signature in database
17 changed files with 185 additions and 34 deletions

View file

@ -0,0 +1,54 @@
name: Smithflix Roku / Build
# Builds the Smithflix Roku channel on every push and attaches a sideloadable
# smithflix-roku.zip to the "latest" release of this repo.
on:
push:
branches: [master]
workflow_dispatch:
permissions:
contents: write
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v7
- name: Setup Node.js
uses: actions/setup-node@v7
with:
node-version: "24"
cache: "npm"
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build
- name: Zip channel
run: |
cd build/staging
zip -qr ../../smithflix-roku.zip .
cd ../..
ls -la smithflix-roku.zip
- name: Publish to the "latest" release
env:
GH_TOKEN: ${{ github.token }}
run: |
gh release delete latest --yes --cleanup-tag || true
gh release create latest smithflix-roku.zip \
--title "Smithflix Roku ${GITHUB_SHA::7}" \
--notes "Sideload on a Roku in developer mode, or package it there for a beta channel." \
--target "${GITHUB_SHA}"
- name: Keep a copy with the run
uses: actions/upload-artifact@v7
with:
name: smithflix-roku
path: smithflix-roku.zip

View file

@ -1,32 +0,0 @@
name: Build
on:
push:
branches: [master]
pull_request:
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v7
- name: Setup Node.js
uses: actions/setup-node@v7
with:
node-version: "24"
cache: "npm"
- name: Install dependencies
run: npm ci
- name: Build jellyfin-roku client project
run: npm run build
- name: Save build output as artifact
uses: actions/upload-artifact@v7
with:
name: Jellyfin-Roku-dev-${{ github.sha }}
path: ${{ github.workspace }}/build/staging

View file

@ -52,6 +52,11 @@ sub init()
m.LoadMyListTask = createObject("roSGNode", "LoadItemsTask")
m.LoadMyListTask.itemsToLoad = "mylist"
' Smithflix: AI picks row (title becomes the playlist name once loaded)
m.LoadAiPicksTask = createObject("roSGNode", "LoadItemsTask")
m.LoadAiPicksTask.itemsToLoad = "aipicks"
m.aiPicksTitle = "Picked for You"
end sub
sub loadLibraries()
@ -268,6 +273,8 @@ function addHomeSection(sectionType as string) as boolean
' Continue Watching items
if sectionType = "resume"
createContinueWatchingRow()
' Smithflix: AI picks sit right below Continue Watching
createAiPicksRow()
return true
end if
@ -1011,6 +1018,64 @@ sub updateMyListItems()
setRowItemSize()
end sub
' createAiPicksRow: Smithflix - placeholder row for the user's AI picks playlist
'
sub createAiPicksRow()
m.expectedRowCount++
if not sectionExists(m.aiPicksTitle)
aiRow = m.top.content.CreateChild("HomeRow")
aiRow.title = m.aiPicksTitle
aiRow.imageWidth = homeRowItemSizes.MOVIE_POSTER[0]
aiRow.cursorSize = homeRowItemSizes.MOVIE_POSTER
end if
m.LoadAiPicksTask.observeField("content", "updateAiPicksItems")
m.LoadAiPicksTask.control = "RUN"
end sub
' updateAiPicksItems: Smithflix - fills the AI picks row, titled with the playlist name
'
sub updateAiPicksItems()
m.processedRowCount++
itemData = m.LoadAiPicksTask.content
metadata = m.LoadAiPicksTask.metadata
m.LoadAiPicksTask.unobserveField("content")
m.LoadAiPicksTask.content = []
oldTitle = m.aiPicksTitle
if not isValidAndNotEmpty(itemData)
removeHomeSection(oldTitle)
return
end if
newTitle = oldTitle
if isValid(metadata) and isValid(metadata.title) then newTitle = metadata.title
row = CreateObject("roSGNode", "HomeRow")
row.title = newTitle
row.imageWidth = homeRowItemSizes.MOVIE_POSTER[0]
row.cursorSize = homeRowItemSizes.MOVIE_POSTER
for each item in itemData
item.usePoster = true
item.imageWidth = row.imageWidth
row.appendChild(item)
end for
m.aiPicksTitle = newTitle
if sectionExists(oldTitle)
m.top.content.replaceChild(row, getSectionIndex(oldTitle))
setRowItemSize()
return
end if
m.top.content.insertChild(row, getSectionIndex(tr("Continue Watching")) + 1)
setRowItemSize()
end sub
' updateContinueWatchingItems: Processes LoadContinueWatchingTask content. Removes, Creates, or Updates continue watching row as needed
'
sub updateContinueWatchingItems()

View file

@ -701,6 +701,64 @@ function loadMyList() as object
return results
end function
' Smithflix: loads the user's "Because you watched ..." playlist (built nightly by the
' jellyfin-ai-playlists service). The playlist name is returned in m.top.metadata.title.
function loadAiPicks() as object
results = []
playlists = api.items.Get({
"userid": m.global.session.user.id,
"includeItemTypes": "Playlist",
"recursive": true,
"nameStartsWith": "Because you watched",
"sortBy": "DateCreated",
"sortOrder": "Descending",
"limit": 1
})
if not isValid(playlists) or not isValidAndNotEmpty(playlists.items) then return results
playlist = playlists.items[0]
playlistID = playlist.LookupCI("id")
if not isValid(playlistID) then return results
m.top.metadata = { title: playlist.LookupCI("name") }
pickData = api.items.Get({
UserId: m.global.session.user.id,
ImageTypeLimit: 1,
EnableImageTypes: `${ImageType.PRIMARY}, ${ImageType.BACKDROP}, ${ImageType.THUMB}`,
Limit: 30,
EnableTotalRecordCount: false,
ParentId: playlistID
})
if not isChainValid(pickData, "Items") then return results
for each item in pickData.Items
tmp = CreateObject("roSGNode", "HomeData")
tmp.posterURL = ItemImageURL(item, ImageType.PRIMARY, imageSizes.HOME_TILE)
json = {
Id: item.LookupCI("Id"),
name: item.LookupCI("name"),
Type: item.LookupCI("Type"),
SeriesName: item.LookupCI("SeriesName"),
SeriesId: item.LookupCI("SeriesId"),
ProductionYear: item.LookupCI("ProductionYear"),
Status: item.LookupCI("Status"),
EndDate: item.LookupCI("EndDate"),
OfficialRating: item.LookupCI("OfficialRating"),
CollectionType: item.LookupCI("CollectionType"),
UserData: item.LookupCI("UserData")
}
json.Append(ItemImageFields(item))
tmp.json = json
results.push(tmp)
end for
return results
end function
function loadPlaylists() as object
results = []
@ -842,6 +900,12 @@ sub loadItems()
return
end if
' Smithflix: per-user AI picks playlist
if isStringEqual(m.top.itemsToLoad, "aipicks")
m.top.content = loadAiPicks()
return
end if
if isStringEqual(m.top.itemsToLoad, "playlists")
m.top.content = loadPlaylists()
return

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View file

@ -561,7 +561,7 @@
</message>
<message>
<source>Pick a Jellyfin server from the local network</source>
<translation>Select an available Jellyfin server from your local network:</translation>
<translation>Select an available Smithflix server from your local network:</translation>
<extracomment>Instructions on initial app launch when the user is asked to pick a server from a list</extracomment>
</message>
<message>

View file

@ -1,6 +1,6 @@
## Channel Details
title=jellyfin
title=Smithflix
major_version=3
minor_version=2
build_version=5