Skip to content

Commit

Permalink
Asset alternates, cycle evenly pill
Browse files Browse the repository at this point in the history
  • Loading branch information
dtcooper committed Jul 28, 2024
1 parent 4b74c16 commit 62f05d2
Show file tree
Hide file tree
Showing 7 changed files with 152 additions and 84 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,11 +129,12 @@ Changes for 2024 based on real world usage in 2023 and feedback
- [x] Check / validate randomization algorithm
- [x] Validated! Review of algorithm provided by [Andy](https://github.com/sagittandy/)
- [x] Mini-player column in asset list view
- [ ] Asset alternates (single asset has 4-5 underlying audio files that are cycled through)
- [x] Asset alternates (single asset has 4-5 underlying audio files that are cycled through)
- [x] Backend done
- [ ] A large clock in the UI
- [ ] Make weights for previous 24 hours... AND reflect that in front-end (day-of
pill) and back-end (sortable)... will require change to `END_DATE_PRIORITY_WEIGHT_MULTIPLIER`
- [x] Added day-of-event pill
- [ ] Stop playing at end of current asset. (Stop playing in 3s with fadeout as well?)

Other things
Expand Down
9 changes: 7 additions & 2 deletions client/src/main/Player.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,15 @@
let items = []
// Medium ignored assets (everything at exists on the screen right now in items list)
let mediumIgnoreIds = new Set()
let mediumIgnoreIds = new Set()
const updateUI = () => {
mediumIgnoreIds = new Set(items.filter((i) => i.type === "stopset").map((s) => s.items.map((a) => a.id)).flat(1))
mediumIgnoreIds = new Set(
items
.filter((i) => i.type === "stopset")
.map((s) => s.items.map((a) => a.id))
.flat(1)
)
items = items // Callback for force re-render
}
Expand Down
10 changes: 5 additions & 5 deletions client/src/main/player/Buttons.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,15 @@
let ledState
$: if (playDisabled) {
ledState = 0 // LED_OFF
ledState = 0 // LED_OFF
} else if (isPaused) {
ledState = 2 // LED_FLASH
ledState = 2 // LED_FLASH
} else if (overdue) {
ledState = 4 // LED_PULSATE_FAST
ledState = 4 // LED_PULSATE_FAST
} else if (overtime) {
ledState = 3 // LED_PULSATE_SLOW
ledState = 3 // LED_PULSATE_SLOW
} else {
ledState = 1 // LED_ON
ledState = 1 // LED_ON
}
$: setLED(ledState)
Expand Down
8 changes: 8 additions & 0 deletions client/src/main/player/List.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,14 @@
>
{asset.rotator.name}
</span>
{#if asset.isAlternate()}
<span class="badge badge-warning border-secondary-content">
Alt #{asset.alternateNumber}
</span>
{/if}
{#if asset.hasEndDateMultiplier}
<span class="badge badge-success border-secondary-content italic"> Ends today!</span>
{/if}
</div>
<div class="truncate text-xl">
{#if asset.playable}
Expand Down
168 changes: 98 additions & 70 deletions client/src/stores/db.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,88 +75,103 @@ class AssetStopsetHydratableObject extends HydratableObject {
}
}

const processFileData = (file, url, filesize, md5sum, db, duration = undefined) => {
const filePath = path.join(assetsDir, file)
const dirname = path.dirname(filePath)
const basename = path.basename(filePath)
const tmpPath = path.join(dirname, `${basename}.tmp`)
return {
duration,
url: `${db.host}${url}`,
localUrl: pathToFileURL(filePath),
path: filePath,
basename,
size: filesize,
md5sum: md5sum,
dirname,
tmpPath,
tmpBasename: path.basename(tmpPath)
}
}

class Asset extends AssetStopsetHydratableObject {
constructor({ file, url, filesize, md5sum, ...data }, db) {
super(data, db)
const filePath = path.join(assetsDir, file)
const dirname = path.dirname(filePath)
const basename = path.basename(filePath)
const tmpPath = path.join(dirname, `${basename}.tmp`)
this.file = {
url: `${db.host}${url}`,
localUrl: pathToFileURL(filePath),
path: filePath,
basename,
size: filesize,
md5sum: md5sum,
dirname,
tmpPath,
tmpBasename: path.basename(tmpPath)
}
this.file = processFileData(file, url, filesize, md5sum, db)
// Ensure they're sorted (backend will do it, but just in case)
this.alternates = this.alternates
.sort((a, b) => a.id - b.id)
.map(({ file, url, filesize, md5sum, duration }) => processFileData(file, url, filesize, md5sum, db, duration))
}

async download() {
const { url, path, size, md5sum, dirname, tmpPath, tmpBasename } = this.file
const files = [this.file, ...this.alternates]

for (let i = 0; i < files.length; i++) {
const file = files[i]
const { url, path, size, md5sum, dirname, tmpPath, tmpBasename } = file

try {
let exists = await fileExists(path)

if (exists) {
// If it exists, just verify sizes match (md5sum was already checked)
if ((await fileSize(path)) !== size) {
console.error(`File size mismatch for ${this.name}. Deleting and trying again.`)
await fs.unlink(path)
exists = false
}
}
if (!exists) {
console.log(`Downloading: ${url}`)
// Accept any certificate during local development (ie, self-signed ones)
await download(url, dirname, {
filename: tmpBasename,
rejectUnauthorized: !IS_DEV,
timeout: { request: 60000 }
})
const actualMd5sum = await md5File(tmpPath)
if (actualMd5sum !== md5sum) {
throw new Error(`MD5 sum mismatch. Actual=${actualMd5sum} Expected=${md5sum}`)
}
fs.rename(tmpPath, path)
}
return true
} catch (e) {
try {
if (await fileExists(tmpPath)) {
await fs.unlink(tmpPath)
let exists = await fileExists(path)

if (exists) {
// If it exists, just verify sizes match (md5sum was already checked)
if ((await fileSize(path)) !== size) {
console.error(`File size mismatch for ${this.name}. Deleting and trying again.`)
await fs.unlink(path)
exists = false
}
}
if (!exists) {
console.log(`Downloading: ${url} (asset: ${this.name}${i > 0 ? `, alt #${i}` : ""})`)
// Accept any certificate during local development (ie, self-signed ones)
await download(url, dirname, {
filename: tmpBasename,
rejectUnauthorized: !IS_DEV,
timeout: { request: 60000 }
})
const actualMd5sum = await md5File(tmpPath)
if (actualMd5sum !== md5sum) {
throw new Error(`MD5 sum mismatch. Actual=${actualMd5sum} Expected=${md5sum}`)
}
fs.rename(tmpPath, path)
}
} catch (e) {
console.error(`Error cleaning up ${tmpPath}\n`, e)
try {
if (await fileExists(tmpPath)) {
await fs.unlink(tmpPath)
}
} catch (e) {
console.error(`Error cleaning up ${tmpPath}\n`, e)
}
console.error(`Error downloading asset ${this.name} @ ${url}\n`, e)
return false
}
console.error(`Error downloading asset ${this.name} @ ${url}\n`, e)
return false
}

return true
}
}

const pickRandomItemByWeight = (objects, endDateMultiplier = null, startTime = null) => {
objects = objects.map((obj) => {
// Apply end date multiplier (if it exists)
if (endDateMultiplier && endDateMultiplier > 0 && obj.end && startTime && obj.end.isSame(startTime, "day")) {
return [obj, obj.weight * endDateMultiplier]
return [obj, obj.weight * endDateMultiplier, true]
} else {
return [obj, obj.weight]
return [obj, obj.weight, false]
}
})
const totalWeight = objects.reduce((s, [, weight]) => s + weight, 0)
const randomWeight = Math.random() * totalWeight
let cumulativeWeight = 0
for (const [obj, weight] of objects) {
for (const [obj, weight, hasEndDateMultiplier] of objects) {
if (weight + cumulativeWeight > randomWeight) {
return obj
return { obj, hasEndDateMultiplier }
}
cumulativeWeight += weight
}
return null
return { obj: null, hasEndDateMultiplier: false }
}

const filterItemsByActive = (obj, dt = null) => {
Expand All @@ -170,9 +185,7 @@ const filterItemsByActive = (obj, dt = null) => {
class Rotator extends HydratableObject {
constructor({ color, ...data }, db) {
super(data, db)
// Ensure's their sorted for evenly_cycle
// TODO confirm sort is needed here... do they come sorted from backedn (try -id in reverse to check),
// ALSO sort asset alternates, and cycle through them similarly
// Ensure's their sorted for evenly_cycle (backend will sort the, but just for sanity)
this.assets = db.assets.filter((a) => a._rotators.includes(this.id)).sort((a, b) => a.id - b.id)
this.color = colors.find((c) => c.name === color)
}
Expand All @@ -193,7 +206,7 @@ class Rotator extends HydratableObject {
}

getRandomAssetForSinglePlay(mediumIgnoreIds = new Set()) {
return this.getAsset(mediumIgnoreIds, undefined, undefined, undefined, true) // forceRandom = true
return this.getAsset(mediumIgnoreIds, undefined, undefined, undefined, true).asset // forceRandom = true
}

getAsset(mediumIgnoreIds = new Set(), hardIgnoreIds = new Set(), startTime, endDateMultiplier, forceRandom = false) {
Expand All @@ -202,10 +215,11 @@ class Rotator extends HydratableObject {
}

let asset = null
let hasEndDateMultiplier = false
let assetListName = ""

const activeAssets = filterItemsByActive(this.assets, startTime)
const cycleEvenly = this.evenly_cycle && !forceRandom
const evenlyCycle = this.evenly_cycle && !forceRandom

// soft ignored = played within a recent amount of time (+ medium and hard)
// medium ignored = exists on screen already (+ hard)
Expand All @@ -214,7 +228,7 @@ class Rotator extends HydratableObject {
const mediumIgnoredAssets = hardIgnoredAssets.filter((a) => !mediumIgnoreIds.has(a.id))

const tries = []
if (!cycleEvenly) {
if (!evenlyCycle) {
const softIgnoreIds = Rotator.getSoftIgnoreIds()
const softIgnoredAssets = mediumIgnoredAssets.filter((a) => !softIgnoreIds.has(a.id))
tries.push(["soft ignored", softIgnoredAssets])
Expand All @@ -228,15 +242,15 @@ class Rotator extends HydratableObject {
}

for (const [name, assets] of tries) {
if (cycleEvenly) {
if (evenlyCycle) {
if (assets.length > 0) {
const assetIdAfter = DB._evenlyCycleRotatorTracker.get(this.id) || 0
asset = assets.find((a) => a.id > assetIdAfter) || assets[0] // Take the first one if we're at end of list
assetListName = `${name} (cycle evenly)`
break
}
} else {
asset = pickRandomItemByWeight(assets, endDateMultiplier, startTime)
;({ obj: asset, hasEndDateMultiplier } = pickRandomItemByWeight(assets, endDateMultiplier, startTime))
assetListName = name
}
if (asset) {
Expand All @@ -250,7 +264,7 @@ class Rotator extends HydratableObject {
console.warn(`Failed to pick an asset entirely! [rotator = ${this.name}]`)
}

return asset
return { asset, hasEndDateMultiplier }
}
}

Expand Down Expand Up @@ -281,14 +295,20 @@ class Stopset extends AssetStopsetHydratableObject {
const items = []
for (const rotator of this.rotators) {
let asset = null
let hasEndDateMultiplier = false
if (rotator.enabled) {
asset = rotator.getAsset(mediumIgnoreIds, hardIgnoreIds, startTime, endDateMultiplier)
;({ asset, hasEndDateMultiplier } = rotator.getAsset(
mediumIgnoreIds,
hardIgnoreIds,
startTime,
endDateMultiplier
))
if (asset) {
hardIgnoreIds.add(asset.id)
startTime = startTime.add(asset.duration, "seconds")
}
}
items.push({ rotator, asset })
items.push({ rotator, asset, hasEndDateMultiplier }) // XXX
}

return new GeneratedStopset(this, items, doneCallback, updateCallback, generatedId)
Expand Down Expand Up @@ -346,19 +366,27 @@ class DB {
} catch {}
}

static markPlayed(asset) {
static markPlayed(asset, rotator = null) {
if (get(config).NO_REPEAT_ASSETS_TIME > 0) {
this._assetPlayTimes.set(asset.id, timestamp())
this._saveAssetPlayTimes()
}
DB._evenlyCycleRotatorTracker.set(asset.rotator.id, asset.id)
DB._saveEvenlyCycleRotatorTracker()
rotator = rotator || asset.rotator
if (rotator.evenly_cycle) {
DB._evenlyCycleRotatorTracker.set(rotator.id, asset.id)
DB._saveEvenlyCycleRotatorTracker()
}
}

static async cleanup() {
if (get(conn).ready) {
// For all non-garbage collected assets, get set of used files
const usedFiles = new Set(Array.from(this._nonGarbageCollectedAssets.values()).map((a) => a.file.basename))
const usedFiles = new Set(
Array.from(this._nonGarbageCollectedAssets.values())
.map((a) => [a.file.basename, ...a.alternates.map((a) => a.basename)])
.flat(1)
)
// add alternates to used files
let deleted = 0
const newFilesToCleanup = new Set()

Expand Down Expand Up @@ -386,7 +414,7 @@ class DB {
generateStopset(startTime, mediumIgnoreIds, endDateMultiplier, doneCallback, updateCallback, generatedId) {
let generated = null
for (let i = 0; i < 3; i++) {
const stopset = pickRandomItemByWeight(filterItemsByActive(this.stopsets, startTime))
const stopset = pickRandomItemByWeight(filterItemsByActive(this.stopsets, startTime)).obj
if (stopset) {
generated = stopset.generate(
startTime,
Expand Down Expand Up @@ -497,6 +525,6 @@ export const clearAssetState = () => {
window.localStorage.removeItem("evenly-cycle-rotator-tracker")
}

export const markPlayed = (asset) => DB.markPlayed(asset)
export const markPlayed = (asset, rotator = null) => DB.markPlayed(asset, rotator)

setInterval(() => DB.cleanup(), IS_DEV ? 15 * 1000 : 45 * 60 * 1000) // Clean up every 45 minutes
Loading

0 comments on commit 62f05d2

Please sign in to comment.