Compare commits

..

No commits in common. "master" and "master" have entirely different histories.

15 changed files with 810 additions and 1091 deletions

4
.gitignore vendored
View File

@ -1,3 +1,3 @@
nate-higgers-assets
anal-probes
lib/sh
.env
scripts

View File

@ -1,18 +0,0 @@
all: build
build: data/songs.json
prod: build validate
# TODO: Move to dist/
lib_js = $(shell find lib -name '*.js' -type f)
data/songs.json: data/songs.js $(lib_js)
node lib/build.js
validate:
ajv -s data/schema.yaml -d data/songs.json --strict=false
links:
lib/sh/getlatest.sh $(i)
dbg:
node -e 'console.debug(require("util").inspect(require("./data/songs.js"), { colors: true, depth: Infinity, maxArrayLength: Infinity }))'

View File

@ -1,72 +0,0 @@
required: [ version, songs ]
$defs:
GWRadioURI:
type: string
description: 'A URI that points to the source file. See `GWMediaPlayer.fromURIToAbsolute()` for how to convert to a URL'
MediaCodec:
type: string
MediaCodecs:
type: array
description: 'An array of codecs contained within the source file. Items should follow RFC6381'
items:
- $ref: "#/$defs/MediaCodec"
properties:
version: { type: integer }
songs:
items:
required: [ id, sources ]
properties:
index:
type: integer
description: 'Original song index'
id:
type: string
metadata:
properties:
title: { type: string }
artist: { type: string }
href:
$ref: "#/$defs/GWRadioURI"
description: 'A URI where the original source can be found, if known. Link may be dead'
_nigid:
description: 'The original song id from nigge.rs.'
type: string
_nigkey:
description: 'Content of _id from the original nigge.rs song entry.'
type: string
misc:
type: integer
description: 'Media bitflags. See BMiscInfo'
tags:
description: 'A list of tags used for filtering.'
type: array
items:
anyOf:
- const: moonman
- const: metadata-prefer-id
- regex: '^radio\-media\-style\-[a-z\-]+'
- type: string
sources:
type: array
items:
- required: [ type, uri ]
properties:
type: { type: string, format: mimetype }
codecs: { "$ref": "#/$defs/MediaCodecs" }
uri: { "$ref": "#/$defs/GWRadioURI" }
previews:
description: >-
This is an array so GWMediaPlayer can better display videos being played as audio only.
The first Item is how the MediaItem *should* be displayed, other Items are alternative versions the player may use.
type: array
items:
- properties:
display_type:
type: integer
description: 'Used to determine how the media should be displayed. See EDisplayType'
type: { "$ref": "#/$defs/MediaCodec" }
codecs: { "$ref": "#/$defs/MediaCodecs" }
uri: { "$ref": "#/$defs/GWRadioURI" }
size: { type: string }

File diff suppressed because one or more lines are too long

897
data/songs.js → generate.js Normal file → Executable file

File diff suppressed because it is too large Load Diff

View File

@ -1,132 +0,0 @@
"use-strict";
const { objSerialize, objBulkSet } = require("./util.js")
const { RadioMetadata } = require("./RadioMetadata.js")
const { RadioMiscInfo } = require("./RadioMiscInfo.js")
/**
* @public
* @constructor
* @params {string} id
* @returns {RadioItem}
*/
const RadioItem = function (id, miscInfo) {
this.id = id
this.metadata = undefined
this.info = undefined
this.misc = miscInfo || undefined
this.tags = undefined
this.sources = []
return this
}
/**
* @public
* @params {string} id
* @returns {RadioItem}
*/
RadioItem.new = id => new RadioItem(id)
/**
* @public
* @params {string|undefined} title
* @params {string|undefined} artist
* @params {URL|string|undefined} [href] href
* @params {string} [extra] extra Playback effects. i.e: Pitch, speed
* @returns {RadioItem}
*/
RadioItem.prototype.setMetadata = function (title, artist, href, extra) {
if (!this.metadata)
this.metadata = RadioMetadata.new()
objBulkSet(this, "metadata", { title, artist, href, extra });
return this
}
/**
* @public
* @params {RadioMetadata} metadata
* @returns {RadioItem}
*/
RadioItem.prototype.addItemInfo = function (metadata) {
if (!this.info)
this.info = []
this.info.push(metadata)
return this
}
/**
* @public
* @params {RadioMiscInfo} miscInfo
* @returns {RadioItem}
*/
RadioItem.prototype.setMiscInfo = function (miscInfo) {
this.misc = miscInfo
return this
}
/**
* @public
* @returns {RadioItem}
*/
RadioItem.prototype.hoistMiscInfo = function () {
return this.misc ? this.misc : this.misc = RadioMiscInfo.new()
}
/**
* @public
* @params {string[]} tags
* @returns {RadioItem}
*/
RadioItem.prototype.addTags = function (tags) { this.tags = this.tags ? [ ...(this.tags), ...tags ] : tags; return this }
/**
* Alias for addTags; prefixes all items with "radio-media-style-"
* @public
* @params {string[]} tags
* @returns {RadioItem}
* @see RadioItem.prototype.addTags
*/
RadioItem.prototype.addStyleTags = function (tags) { return this.addTags(tags.map(a => `radio-media-style-${a}`)); return this }
/**
* @public
* @params {RadioSource} source
* @returns {RadioItem}
*/
RadioItem.prototype.addSource = function (source) { this.sources.push(source); return this }
/**
* Set metadata for songs that were previously on nigge.rs; adds the "niggers" tag.
* @public
* @params {string|undefined} nigid
* @params {string|undefined} nigkey
* @returns {RadioItem}
*/
RadioItem.prototype.setNiggadata = function (nigid, nigkey) {
this.setMetadata()
this.metadata._nigid = nigid
this.metadata._nigkey = nigkey
//this.addTags([ "niggers" ])
this.hoistMiscInfo().bNiggersSong()
return this
}
/**
* @deprecated
* @returns {Object}
*/
RadioItem.prototype.serialize = function (index) {
const self = this
return objSerialize(this, [
"id",
"tags",
"metadata"
], obj => ({
index,
...obj,
"misc": self.misc ? self.misc.serialize() : undefined,
"info": self.info?.length ? self.info.map(info => info.serialize()) : undefined,
"sources": self.sources.map(source => source.serialize())
}))
}
module.exports = { RadioItem }

View File

@ -1,199 +0,0 @@
"use-strict";
const { objSerialize, fromConstToPascal } = require("./util.js")
/**
* @public
* @readonly
*/
const BMetadataType = {
"AUDIO": 0b1,
"VIDEO": 0b10,
"ORIGINAL": 0b100,
"ALT": 0b1000,
"OFFICIAL": 0b10000,
"EXPLICIT": 0b100000,
"PREFERED": 0b1000000,
"PSEUDONYM": 0b10000000,
"UNAVAILABLE": 0b100000000,
"PREMIUM": 0b1000000000
}
/**
* @public
* @constructor
* @params {string|undefined} title
* @params {string|undefined} artist
* @params {URL|string|undefined} [href] href
* @params {string} [extra] extra Playback effects. i.e: Pitch, speed
* @returns {RadioMetadata}
*/
const RadioMetadata = function (title, artist, href, extra) {
this.info = undefined
this.title = title || undefined
this.artist = artist || undefined
this.href = href || undefined
this.extra = extra || undefined
return this
}
/**
* @public
* @params {string|undefined} title
* @params {string|undefined} artist
* @params {URL|string|undefined} [href] href
* @params {string} [extra] extra Playback effects. i.e: Pitch, speed
* @returns {RadioMetadata}
*/
RadioMetadata.new = (title, artist, href, extra) => new RadioMetadata(title, artist, href, extra)
/**
* Alias for new(...).bVideo().bAudio()
* @public
* @params {string|undefined} title
* @params {string|undefined} artist
* @params {URL|string|undefined} [href] href
* @params {string} [extra] extra Playback effects. i.e: Pitch, speed
* @returns {RadioMetadata}
*/
RadioMetadata.newLAV = (title, artist, href, extra) => (new RadioMetadata(title, artist, href, extra)).bVideo().bAudio()
/**
* Alias for new(...).bVideo()
* @public
* @params {string|undefined} title
* @params {string|undefined} artist
* @params {URL|string|undefined} [href] href
* @params {string} [extra] extra Playback effects. i.e: Pitch, speed
* @returns {RadioMetadata}
*/
RadioMetadata.newLV = (title, artist, href, extra) => (new RadioMetadata(title, artist, href, extra)).bVideo()
/**
* Alias for new(...).bAudio()
* @public
* @params {string|undefined} title
* @params {string|undefined} artist
* @params {URL|string|undefined} [href] href
* @params {string} [extra] extra Playback effects. i.e: Pitch, speed
* @returns {RadioMetadata}
*/
RadioMetadata.newLA = (title, artist, href, extra) => (new RadioMetadata(title, artist, href, extra)).bAudio()
/**
* @public
* @params {string} watch_id
* @returns {RadioMetadata}
*/
RadioMetadata.createYouTubeURI = id => `x-yt://${id}`
/**
* @public
* @params {string} id
* @returns {RadioMetadata}
*/
RadioMetadata.createNicoNicoURI = id => `x-nn://${id}`
/**
* @public
* @params {string} id
* @returns {RadioMetadata}
*/
RadioMetadata.createInternetArchiveURI = id => `x-ia://${id}`
/**
* @public
* @params {bigint|string} post_id
* @params {string} username
* @returns {RadioMetadata}
*/
RadioMetadata.createTikTokURI = (post_id, username) => `x-tt://${username}/${post_id}`
/**
* @public
* @params {string} track
* @params {string} artist
* @returns {RadioMetadata}
*/
RadioMetadata.createBandcampURI = (track, artist) => `x-bc://${artist}/${track}`
/**
* @public
* @params {string} track
* @params {string} artist
* @returns {RadioMetadata}
*/
RadioMetadata.createSoundcloudURI = (track, artist) => `x-sc://${artist}/${track}`
/**
* @public
* @params {number} post_id
* @returns {RadioMetadata}
*/
RadioMetadata.createBooruSoyURI = id => `x-bs://${id}`
/**
* @public
* @params {number} track_id
* @params {number|undefined} album_id
* @returns {RadioMetadata}
*/
RadioMetadata.createAppleMusicURI = (track_id, album_id) => `x-am://${track_id}${album_id ? '/' + album_id : ''}`
/**
* @public
* @params {number} match_id
* @returns {RadioMetadata}
*/
RadioMetadata.createShazamURI = match_id => `x-sz://${match_id}`
/**
* @public
* @params {string} id
* @returns {RadioMetadata}
*/
RadioMetadata.createWikipediaURI = id => `x-wp://${id}`
/**
* @public
* @params {number} string
* @returns {RadioMetadata}
*/
RadioMetadata.createWikimediaCommonsURI = id => `x-wc://${id}`
/**
* Used to differentiate identical info bits for different info items
* @public
* @params {string} key
* @returns {RadioMetadata}
*/
RadioMetadata.prototype.setFor = function (key) {
this.for = key
return this
}
/**
* @deprecated
* @params {RadioSource} radioSource
* @returns {Object}
*/
RadioMetadata.prototype.serialize = function () {
return objSerialize(this, [
"info",
"for",
"title",
"artist",
"href",
"extra"
])
}
for (const key in BMetadataType) {
RadioMetadata.prototype[`b${fromConstToPascal(key)}`] = function () {
//this.info = (this.info || 0) | BMetadataType[key]
this.info |= BMetadataType[key]
return this
}
}
module.exports = { RadioMetadata, BMetadataType }

View File

@ -1,52 +0,0 @@
"use-strict";
const { fromConstToPascal } = require("./util.js")
/**
* @public
* @readonly
*/
const BMiscInfo = {
"EXPLICIT": 0b1,
"SPLASH_SONG": 0b10,
"NIGGERS_SONG": 0b100,
"AMOGUS_SONG": 0b1000,
"METADATA_PREFER_ID": 0b10000,
"ALT_MEDIA": 0b100000
}
/**
* @public
* @constructor
* @params {string|undefined} title
* @params {string|undefined} artist
* @params {URL|string|undefined} [href] href
* @params {string} [extra] extra Playback effects. i.e: Pitch, speed
* @returns {RadioMetadata}
*/
const RadioMiscInfo = function (value) {
this.miscInfo = value || 0
return this
}
/**
* @public
* @params {number|undefined} value
* @returns {RadioPlaybackInfo}
*/
RadioMiscInfo.new = value => new RadioMiscInfo(value)
/**
* @public
* @returns {number|undefined}
*/
RadioMiscInfo.prototype.serialize = function () { return this.miscInfo || undefined; }
for (const key in BMiscInfo) {
RadioMiscInfo.prototype[`b${fromConstToPascal(key)}`] = function () {
//this.miscInfo = (this.miscInfo || 0) | BMiscInfo[key]
this.miscInfo |= BMiscInfo[key]
return this
}
}
module.exports = { RadioMiscInfo, BMiscInfo }

View File

@ -1,37 +0,0 @@
"use-strict";
const { objSerialize } = require("./util.js")
/**
* @public
* @constructor
* @returns {RadioPlaybackInfo}
*/
const RadioPlaybackInfo = function () {
this.volume = undefined
return this
}
/**
* @public
* @returns {RadioPlaybackInfo}
*/
RadioPlaybackInfo.new = () => new RadioPlaybackInfo()
/**
* @public
* @param {number} volume - Playback volume. Crude version of ReplayGain
* @returns {RadioPlaybackInfo}
*/
RadioPlaybackInfo.prototype.setPlaybackVolume = function (volume) { this.volume = volume; return this; }
/**
* @deprecated
* @returns {Object}
*/
RadioPlaybackInfo.prototype.serialize = function () {
return objSerialize(this, [
"volume"
])
}
module.exports = { RadioPlaybackInfo }

View File

@ -1,62 +0,0 @@
"use-strict";
const { objSerialize, notNullElse } = require("./util.js")
/**
* @public
* @readonly
* @enum {number}
*/
const EDisplayType = {
"NATIVE": -1,
"LANDSCAPE": 0,
"SQUARE": 1,
"STRETCH": 2
}
/**
* @public
* @constructor
* @params {string} uri
* @params {string} type
* @params {string[]} codecs
* @params {String} size
* @params {EDisplayType} [displayType=null] size
* @returns {RadioPreview}
*/
const RadioPreview = function (uri, type, codecs, size, displayType=null) {
this.uri = uri
this.type = type
this.codecs = codecs?.length ? codecs : undefined
this.size = size.replace(/x/g, "×")
this.display_type = displayType
return this
}
/**
* @public
* @params {string} uri
* @params {string} type
* @params {string[]} codecs
* @params {String} size
* @params {EDisplayType} [displayType=null] size
* @returns {RadioPreview}
*/
RadioPreview.new = (uri, type, codecs, size, displayType) => new RadioPreview(uri, type, codecs, size, displayType)
/**
* @deprecated
* @params {RadioSource} radioSource
* @returns {Object}
*/
RadioPreview.prototype.serialize = function (radioSource) {
this.display_type = notNullElse(this.display_type, radioSource.type.startsWith("video/") ? EDisplayType.NATIVE : EDisplayType.SQUARE)
return objSerialize(this, [
"display_type",
"type",
"codecs",
"size",
"uri"
])
}
module.exports = { RadioPreview, EDisplayType }

View File

@ -1,126 +0,0 @@
"use-strict";
const { objSerialize, objBulkSet } = require("./util.js")
const { RadioMetadata } = require("./RadioMetadata.js")
const { RadioPreview } = require("./RadioPreview.js")
/**
* @public
* @constructor
* @params {string} type
* @params {string[]} [codecs] codecs
* @returns {RadioSource}
*/
const RadioSource = function (type, codecs) {
this.type = type
this.uri = null
this.previews = []
this.setTypeWCodec(type, codecs)
return this
}
/**
* @public
* @params {string} type
* @params {string[]} [codecs] codecs
* @returns {RadioSource}
*/
RadioSource.new = (type, codecs) => new RadioSource(type, codecs)
/**
* @public
* @params {string} uri
* @returns {RadioSource}
*/
RadioSource.prototype.setURI = function (uri) { this.uri = uri; return this }
/**
* Set metadata that specifically applies to *this* source. Used for items where the audio has a different attribution than
* the video.
* @public
* @params {string|undefined} title
* @params {string|undefined} artist
* @params {URL|string|undefined} [href] href
* @params {string} [extra] extra Playback effects. i.e: Pitch, speed
* @returns {RadioSource}
*/
RadioSource.prototype.setMetadata = function (title, artist, href, extra) {
if (!this.metadata)
this.metadata = RadioMetadata.new()
objBulkSet(this, "metadata", { title, artist, href, extra });
return this
}
/**
* @public
* @params {RadioPlaybackInfo} playbackInfo
* @returns {RadioSource}
*/
RadioSource.prototype.setPlaybackInfo = function (playbackInfo) { this.playbackinfo = playbackInfo; return this; }
/**
* @public
* @params {string} uri
* @returns {RadioSource}
*/
RadioSource.prototype.setTypeWCodec = function (type, codecs) {
this.type = type
this.codecs = codecs?.length ? codecs : undefined
return this
}
/**
* @public
* @params {string} uri
* @params {string} type
* @params {string} size
* @params {EDisplayType} [displayType] displayType
* @returns {RadioSource}
*/
/**
* @public
* @params {string} uri
* @params {string} type
* @params {string[]} codecs
* @params {String} size
* @params {EDisplayType} [displayType] displayType
* @returns {RadioSource}
*/
RadioSource.prototype.addPreview = function () {
let uri, type, codecs, size, displayType
if (typeof arguments[0] == "string" && typeof arguments[1] == "string" && arguments[2] instanceof Array) {
uri = arguments[0]
type = arguments[1]
codecs = arguments[2]
size = arguments[3]
displayType = arguments[4]
} else if (typeof arguments[0] == "string" && typeof arguments[1] == "string" && typeof arguments[2] == "string") {
uri = arguments[0]
type = arguments[1]
size = arguments[2]
displayType = arguments[3]
} else
throw new TypeError("Could not recognize arguments. Expecting [ LString, LString, LString, ...?] or [ LString, LString, LString[], ...?]")
this.previews.push(RadioPreview.new(uri, type, codecs, size, displayType))
return this
}
/**
* @deprecated
* @returns {Object}
*/
RadioSource.prototype.serialize = function () {
return objSerialize(this, [
"type",
"codecs",
"uri"
], (obj, self) => ({
...obj,
"metadata": self.metadata ? self.metadata.serialize() : undefined,
"playbackinfo": self.playbackinfo ? self.playbackinfo.serialize() : undefined,
"previews": this.previews.map(preview => preview.serialize(obj))
}))
}
module.exports = { RadioSource }

View File

@ -1,17 +0,0 @@
"use-strict";
const fs = require("fs")
const { arrSerializeSort, toRadioSongs } = require("./util.js")
const { mediaItemsLint, mediaItemsSerialize } = require("./lint.js")
let songs = arrSerializeSort(require("../data/songs.js"))
mediaItemsLint(songs)
songs = toRadioSongs(songs)
console.info(`I: Version ${songs.version} with ${songs.songs.length} entries`)
fs.writeFileSync("./data/songs.json", JSON.stringify(songs))
//console.info(require("util").inspect(mediaItems[0], { colors: true, depth: Infinity }))
//fs.writeFileSync("./data/songs.json", JSON.stringify(radioObj))
//fs.writeFileSync("./data/songs.js", `glowersRadioSongsCallback(${JSON.stringify(radioObj)})`)
//fs.writeFileSync("./data/songs.html", `<html><script>glowersRadioSongsCallback(${JSON.stringify(radioObj)})</script></html>`)

View File

@ -1,50 +0,0 @@
"use-strict";
const { BMiscInfo } = require("./RadioMiscInfo.js")
const { EDisplayType } = require("./RadioPreview.js")
const CODEC_WARNINGS = {
"video/mp4": [
[ "warn", "avc1", "vague, however browsers will accept it. See MDN for format details" ],
[ "error", "mp4a", "too vague; Browsers will not accept it! See MDN for format details" ],
[ "error", "aac", "invalid! The correct codec is mp4a. See MDN for format details" ]
]
}
const CODEC_SUGGESTIONS = {
"video/mp4": 'codecs may be "avc1" with "mp4a.40.2" (AAC-LC)',
"audio/mp3": 'codec is probably "mp3"'
}
const mediaItemsLint = mediaItems => {
for (const item of mediaItems) {
const hasAudioSource = item.sources.some(a => a.type.startsWith("audio/"))
for (const index in item.sources) {
const source = item.sources[index]
if (!hasAudioSource && source.type.startsWith("video/") && !source.previews.some(item => item.display_type != EDisplayType.NATIVE)) {
const logType = (item?.misc & BMiscInfo.ALT_MEDIA) ? "warn" : "error"
console.warn(`${logType[0].toUpperCase()}: [previews] [${item.id}:${index}] No audio fallback preview set!`)
}
if (source.codecs?.length)
for (const [ logType, codec, reason ] of (Object.hasOwn(CODEC_WARNINGS, source.type) ? CODEC_WARNINGS[source.type] : [])) {
if (!source.codecs.includes(codec))
continue
const msg = `${logType[0].toUpperCase()}: [codecs] [${item.id}:${index}] The declaration "${codec}" is ${reason}`
console[logType](msg)
}
else
console.warn(`W: [codecs] [${item.id}:${index}] No codec declarations are set!${Object.hasOwn(CODEC_SUGGESTIONS, source.type)?` The ${CODEC_SUGGESTIONS[source.type]}, check with ffprobe.`:''}`)
for (const p_index in source.previews) {
const preview = item.sources[index].previews[p_index]
if (!preview.type)
console.warn(`W: [previews] [${item.id}:${index}] No type set for preview ${p_index} source ${index} of ${item.id}`)
if (!preview.size)
console.warn(`W: [previews] [${item.id}:${index}] No size set for preview ${p_index} source ${index} of ${item.id}`)
}
}
}
}
module.exports = { mediaItemsLint }

View File

@ -1,51 +0,0 @@
"use-strict";
const objSerialize = (obj, keys, cb) => {
let a = Object.create(null)
for (const key of keys)
a[key] = obj[key]
if (cb)
return cb(a, obj)
return a
}
const objBulkSet = (obj, target, pairs) => {
if (typeof obj[target] != "object")
obj[target] = Object.create(null)
for (const key in pairs)
if (pairs[key] != undefined)
obj[target][key] = pairs[key]
}
const fromConstToPascal = string => {
let a = ''
let b = true
for (const char of string)
if (char == '_')
b = true
else {
a += b ? char : char.toLowerCase()
b = false
}
return a
}
const notNullElse = (a, fallback) => a != null && a != undefined ? a : fallback
const arrSerializeSort = array => array.filter(Boolean).map((a,i) => a.serialize(i)).sort((a,b) => a.id.localeCompare(b.id))
const SCHEMA_VERSION = 8
const toRadioSongs = songs => ({
"version": SCHEMA_VERSION,
songs
})
module.exports = {
SCHEMA_VERSION,
objSerialize,
objBulkSet,
arrSerializeSort,
fromConstToPascal,
notNullElse,
toRadioSongs
}

182
readme.md
View File

@ -1,50 +1,148 @@
# glowers-radio
This repo contains backing information for Glowers Radio.
## Contributing
If you have a song in mind that would fit on Glowers Radio, you are free to create a PR or send in a diff for `generate.js`.
The living schema is documented [below](#format) and mostly maps 1:1 to `MediaItem`, you should also check the `MediaItem` for the
the media that is most similar to what you wish to add.
## Best Practices
- Use an alpha-numeric id. Don't use dashes or hyphens
- Add proper song metadata when appropriate. Attribution data should be added when possible
- Keep all media assets under 10MB, a single item's assets should not be over 15MB.
- Add codec information for your sources.
- Mimic other items if you're unsure about something.
- Use an alpha-numeric id. Don't use dashes or hyphens; Uppercase characters *may* be used when appropriate.
- Declare metadata when available. If you do not know the original source, try to find it first.
- Metadata **must** be accurate and not an artists rendition of the actual metadata.
- Items that aren't primarily music should be tagged with `misc`.
- Use the `metadata-prefer-id` tag if the actual title would be too long, cubersome, or unrelated. i.e:
- `slamtris`: Slamtris - Quad City DJs vs Hirokazu Tanaka *(cumbersome)*
- `dngyamom`: Dynamite by BTS but it's Doin' Your Mom *(cumbersome)*
- `thisistheinfowar`: In the House, In a Heartbeat *(unrelated)*
- `alexjones`: Alex Jones Remix: Renai Circulation *(cumbersome)*
- `ywnbaw`: Professor proves SICKS are the best weapons *(unrelated)*
- `whenblackissus`: Among Us theme song but it's in the style of Metallica's Black Album *(long and cumbersome)*
- `niggatorial`: I'm The 2007 YouTube Tutorial *(cumbersome)*
- `honorary`: Erika *(unrelated)*
## Examples
<br/>
```js
RadioItem.new("hummerguy")
.addItemInfo(RadioMetadata.newLAV("hummer guy ytpmv", "gud boi", RadioMetadata.createYouTubeURI("A2Fd4NHreI0")).bOfficial())
.addItemInfo(RadioMetadata.newLA("Skrillex - Scary Monsters and Nice Sprites (Sihk Happy Hardcore Remix)", "SIHK", RadioMetadata.createYouTubeURI("Z01Tsgwe2dQ")).setFor(0).bOriginal().bOfficial())
.addItemInfo(RadioMetadata.newLA(null, null, RadioMetadata.createSoundcloudURI("skrillex-scary-monsters-nice-sprites-sihk-happy-hardcore-remix-free-dl", "sihk66")).setFor(0).bOriginal().bOfficial().bAlt())
.addSource(RadioSource.new("video/mp4", [ "avc1.4D4029", "mp4a.40.2" ])
.setURI("mxc://glowers.club/jnmPzoUhxCTrvRZmpxmGXYMr")
.addPreview("mxc://glowers.club/AmkQLtUuuCixPKNzkugAkiHh", "image/jpg", "800x449"))
.addSource(RadioSource.new("audio/mp3", [ "mp3" ])
.setURI("mxc://glowers.club/wtsFmPEzerDPdzOSQraBzupN")
.addPreview("mxc://glowers.club/UPPtHMbKXrEaeHLDYovVkZRt", "image/jpg", "440x440")),
- Use `image/png`, `image/jpg`, `image/gif`, `audio/mp3`, and `video/mp4`. Files *should* be under 10MB and be of reasonable or tolerable quality
- Try not to use reuploads, remasters, or edits, as the source; Unless the edit or remaster is the actual media.
- Don't use videos where audio alongside artwork would be functionally identical. Lyrics videos are ok
- Sources *should* contain codec information; if you are unsure of what correct codecs to declare, do not declare any or ask for help.
No codec declarations are better than incorrect codec declarations. See [MDN](https://developer.mozilla.org/en-US/docs/Web/Media/Formats/codecs_parameter#basic_syntax) for details
- All content *should* be hosted on Glowers Club. If you do not Glow, then your sources will be reuploaded if your Item(s) are accepted
<br/>
- `video/` **must** have a thumbnail preview.
- `video/` *should* have a `EDisplayType.SQUARE` fallback for audioOnly, and for when the agent does not support the provided `video/`.
- Animated previews **must** always have a `image/gif` fallback.
- Avoid using `EDisplayType.LANDSCAPE` when possible.
- `EDisplayType.SQUARE` previews **must** be exactly `1:1`; `EDisplayType.LANDSCAPE` previews *should* be exactly `16:9`, or **must** be a close equivalent
## Format
The `songs.json` format **is only intended for use for Glowers Radio**, and is effectively a living standard as a result. **Live data may change drastically without warning**
If you do wish to use this data for your own purposes, i.e: to make a custom player, you should be caching the songs list so your application has something to fallback to if a breaking change to the format is made.
```yaml
required: [ version, songs ]
$defs:
GWRadioURI:
type: uri
description: 'A URI that points to the source file. See `GWMediaPlayer.fromURIToAbsolute()` for how to convert to a URL'
MediaCodecs:
type: array
description: 'An array of codecs contained within the source file. Items should follow RFC6381'
items:
- type: string
properties:
version: { type: integer }
songs:
items:
required: [ id, sources ]
properties:
id:
type: string
tags:
description: 'A list of tags used for filtering.'
type: array
items:
- const: sus
description: >-
Media information will render in the Among Us Font.
In imposter mode, only songs tagged with sus will be in shuffle
- const: niggers
description: >-
Media that was originally available through the now-defunct nigge.rs online radio.
Tagged items may contain the original _nigid and/or _nigkey as metadata
$seeAlso:
- '#/properties/songs/items/properties/metadata/properties/_nigid'
- '#/properties/songs/items/properties/metadata/properties/_nigkey'
- const: splash-song
description: 'Media that can play on the Glowers Club splash page.'
- const: misc
description: 'Media that does not significantly contain music.'
- const: moonman
description: 'Moonman songs'
- const: metadata-prefer-id
description: >-
Use the Media id instead of the metadata title for MediaSession.
Primarily for media where the source title does not match the assigned id
- regex: '^radio\-media\-style\-[a-z\-]+'
description: >-
Used to add or remove styling for particular Media, such as to remove the
Album Art box or disable background blur.
- type: string
description: 'Tags may also be arbitrary'
metadata:
required: [ title, artist ]
properties:
title: { type: string }
artist: { type: string }
href:
description: 'A URL the original source can be found, if known. Link may be dead'
type: string
format: url
_nigid:
$seeAlso: '#/properties/songs/items/properties/tags/items/1'
description: 'The original song id from nigge.rs.'
type: string
_nigkey:
$seeAlso: '#/properties/songs/items/properties/tags/items/1'
description: 'Content of _id from the original nigge.rs song entry.'
type: string
sources:
type: array
items:
- required: [ type, url ]
properties:
type: { type: string, format: mimetype }
codecs: { "$ref": "#/$defs/MediaCodecs" }
uri: { "$ref": "#/$defs/GWRadioURI" }
previews:
description: >-
This is an array so GWMediaPlayer can better display videos being played as audio only.
The first Item is how the MediaItem *should* be displayed, other Items are alternative versions the player may use.
type: array
items:
- properties:
display_type:
description: 'Used to determine how the media should be displayed. See GWMediaPlayer.EDisplayType'
type: string
type: { type: string, format: mimetype }
codecs: { "$ref": "#/$defs/MediaCodecs" }
uri: { "$ref": "#/$defs/GWRadioURI" }
size: { type: string }
```
```js
RadioItem.new("kramer")
.setMiscInfo(RadioMiscInfo.new().bExplicit().bMetadataPreferId())
.addItemInfo(RadioMetadata.newLAV("wholesome ytpmv 3", "KrazedDonut", RadioMetadata.createYouTubeURI("LVdJ6cqqiUA")).bOfficial().bUnavailable().bExplicit())
.addItemInfo(RadioMetadata.newLA("Spin ye Bottle (Minigame)", "Jake Kaufman", RadioMetadata.createBandcampURI("spin-ye-bottle-minigame", "virt")).setFor(0).bOriginal().bOfficial())
.addItemInfo(RadioMetadata.newLA(null, null, RadioMetadata.createAppleMusicURI(1170509291)).setFor(0).bOriginal().bOfficial().bAlt())
.addSource(RadioSource.new("video/mp4", [ "avc1.4D4029", "mp4a.40.2" ])
.setURI("mxc://glowers.club/FztaIVnDJeHkYQfZmdHFOdnP")
.addPreview("mxc://glowers.club/FKCKRytGQEFRJUxTfeKjXmXz", "image/jpg", "800x450"))
.addSource(RadioSource.new("audio/mp3", [ "mp3" ])
.setURI("mxc://glowers.club/hqRzxaQMWfQoZGpiPIbyNSEk")
.addPreview("mxc://glowers.club/JurRLPteOyFLrfJoRJuzbZkH", "image/jpg", "500x500")),
```
```js
RadioItem.new("blsdm")
.setMiscInfo(RadioMiscInfo.new().bExplicit())
.addItemInfo(RadioMetadata.newLA("Black Lives (Still Don't) Matter", "Moonman").bExplicit().bPseudonym())
.addSource(RadioSource.new("audio/mp3", [ "mp3" ])
.setURI("mxc://glowers.club/GfcCntGLSZsYkkzzkaoJrUWx")
.addPreview("mxc://glowers.club/yyWJVkClXhWIFqgayFFtZZDJ", "image/jpg", "500x500")),
```
## Integrations
See [data/schema.yaml](data/schema.yaml) for JSONSchema documentation. You may also want to inspect the code within [lib/](lib/)