forked from jon/glowers-radio
Compare commits
34 Commits
Author | SHA1 | Date | |
---|---|---|---|
23c45cc1c4 | |||
7081636352 | |||
3d9f04a083 | |||
fa62359e34 | |||
e4d86fdf57 | |||
ec6ecdbb73 | |||
2f540f3f90 | |||
4ba310dafe | |||
a7b20aa64c | |||
c7b4ad9cc8 | |||
b2e245d4e9 | |||
2a96a8a9a3 | |||
756ca814c8 | |||
1f9d0c406e | |||
baba51f9f1 | |||
f49b9a88a3 | |||
e2b2264e63 | |||
31c12e7199 | |||
891aedfbbb | |||
296a80074c | |||
a9e92b4ccb | |||
ea7dee1392 | |||
3ee21638fd | |||
674e763168 | |||
67eea5900c | |||
0e770d78e8 | |||
346cc478c9 | |||
988014a867 | |||
d7588436e2 | |||
25a954c3db | |||
b0259dbb60 | |||
4304670ea9 | |||
622e180573 | |||
69488a2ccd |
4
.gitignore
vendored
4
.gitignore
vendored
@ -1,3 +1,3 @@
|
||||
nate-higgers-assets
|
||||
anal-probes
|
||||
scripts
|
||||
lib/sh
|
||||
.env
|
||||
|
18
Makefile
Normal file
18
Makefile
Normal file
@ -0,0 +1,18 @@
|
||||
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 }))'
|
72
data/schema.yaml
Normal file
72
data/schema.yaml
Normal file
@ -0,0 +1,72 @@
|
||||
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 }
|
897
generate.js → data/songs.js
Executable file → Normal file
897
generate.js → data/songs.js
Executable file → Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
132
lib/RadioItem.js
Normal file
132
lib/RadioItem.js
Normal file
@ -0,0 +1,132 @@
|
||||
"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 }
|
199
lib/RadioMetadata.js
Normal file
199
lib/RadioMetadata.js
Normal file
@ -0,0 +1,199 @@
|
||||
"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 }
|
52
lib/RadioMiscInfo.js
Normal file
52
lib/RadioMiscInfo.js
Normal file
@ -0,0 +1,52 @@
|
||||
"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 }
|
37
lib/RadioPlaybackInfo.js
Normal file
37
lib/RadioPlaybackInfo.js
Normal file
@ -0,0 +1,37 @@
|
||||
"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 }
|
62
lib/RadioPreview.js
Normal file
62
lib/RadioPreview.js
Normal file
@ -0,0 +1,62 @@
|
||||
"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 }
|
126
lib/RadioSource.js
Normal file
126
lib/RadioSource.js
Normal file
@ -0,0 +1,126 @@
|
||||
"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 }
|
17
lib/build.js
Normal file
17
lib/build.js
Normal file
@ -0,0 +1,17 @@
|
||||
"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>`)
|
50
lib/lint.js
Normal file
50
lib/lint.js
Normal file
@ -0,0 +1,50 @@
|
||||
"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 }
|
51
lib/util.js
Normal file
51
lib/util.js
Normal file
@ -0,0 +1,51 @@
|
||||
"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
182
readme.md
@ -1,148 +1,50 @@
|
||||
# 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; 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)*
|
||||
- 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.
|
||||
|
||||
<br/>
|
||||
## Examples
|
||||
|
||||
- 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("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")),
|
||||
```
|
||||
|
||||
```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/)
|
||||
|
Loading…
Reference in New Issue
Block a user