From 0e770d78e817591996860e8f7d776704d7b57a57 Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 5 Feb 2023 14:05:59 +0000 Subject: [PATCH] .gitignore: refactor. add tour, oppanmachine --- .gitignore | 3 +- Makefile | 11 + data/schema.yaml | 72 +++ data/songs.js | 1043 ++++++++++++++++++++++++++++++++++++++ lib/RadioItem.js | 132 +++++ lib/RadioMetadata.js | 192 +++++++ lib/RadioMiscInfo.js | 52 ++ lib/RadioPlaybackInfo.js | 37 ++ lib/RadioPreview.js | 62 +++ lib/RadioSource.js | 126 +++++ lib/build.js | 17 + lib/lint.js | 50 ++ lib/util.js | 51 ++ 13 files changed, 1846 insertions(+), 2 deletions(-) create mode 100644 Makefile create mode 100644 data/schema.yaml create mode 100644 data/songs.js create mode 100644 lib/RadioItem.js create mode 100644 lib/RadioMetadata.js create mode 100644 lib/RadioMiscInfo.js create mode 100644 lib/RadioPlaybackInfo.js create mode 100644 lib/RadioPreview.js create mode 100644 lib/RadioSource.js create mode 100644 lib/build.js create mode 100644 lib/lint.js create mode 100644 lib/util.js diff --git a/.gitignore b/.gitignore index 8ceea24..5ab84ea 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,2 @@ -nate-higgers-assets anal-probes -scripts +lib/sh diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..0438e16 --- /dev/null +++ b/Makefile @@ -0,0 +1,11 @@ +all: | build +prod: | build validate + +build: + node lib/build.js +validate: + ajv -s data/schema.yaml -d data/songs.json +links: + lib/sh/getlatest.sh $(i) +dbg: + node -e 'console.debug(require("util").inspect(require("./data/songs.js"), { colors: true, depth: Infinity, maxArrayLength: Infinity }))' diff --git a/data/schema.yaml b/data/schema.yaml new file mode 100644 index 0000000..667b68a --- /dev/null +++ b/data/schema.yaml @@ -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 } diff --git a/data/songs.js b/data/songs.js new file mode 100644 index 0000000..75e80c8 --- /dev/null +++ b/data/songs.js @@ -0,0 +1,1043 @@ +"use-stricts"; +const { RadioItem } = require("../lib/RadioItem.js") +const { RadioMetadata } = require("../lib/RadioMetadata.js") +const { RadioMiscInfo } = require("../lib/RadioMiscInfo.js") +const { RadioPlaybackInfo } = require("../lib/RadioPlaybackInfo.js") +const { RadioPreview, EDisplayType } = require("../lib/RadioPreview.js") +const { RadioSource } = require("../lib/RadioSource.js") + +module.exports = [ + + RadioItem.new("nightinjunitaki") + .setMiscInfo(RadioMiscInfo.new().bSplashSong()) + .addItemInfo(RadioMetadata.newLA("Night In Junitaki", "Wünsche", RadioMetadata.createSoundcloudURI("night-in-junitaki", "samuelwunsche")).bOfficial()) + .addSource(RadioSource.new("audio/mp3", [ "mp3" ]) + .setURI("x-gwm://wiki/night_in_junitaki_-_waves.mp3") + .addPreview("mxc://glowers.club/HCiiIVMOekjPZtiEaexDdUsD", "image/jpg", "400x400")), + + RadioItem.new("goodbyeautumn") + .setMiscInfo(RadioMiscInfo.new().bSplashSong()) + .addItemInfo(RadioMetadata.newLA("Goodbye Autumn", "Tomppabeats", RadioMetadata.createBandcampURI("goodbye-autumn", "tomppabeats")).bOfficial()) + .addSource(RadioSource.new("audio/mp3", [ "mp3" ]) + .setURI("x-gwm://wiki/goodbye_autumn.mp3") + .addPreview("mxc://glowers.club/JslhZSsDpgtrnmWhSqYMEUDL", "image/jpg", "500x500")), + + RadioItem.new("balmy") + .setMiscInfo(RadioMiscInfo.new().bSplashSong()) + .addItemInfo(RadioMetadata.newLA("Balmy", "90sFlav", RadioMetadata.createBandcampURI("balmy", "90sflav")).bOfficial()) + .addSource(RadioSource.new("audio/mp3", [ "mp3" ]) + .setURI("x-gwm://wiki/balmy90sblav.mp3") + .addPreview("mxc://glowers.club/kDNCBjvQxfPvtJVmfEMmcdov", "image/jpg", "999x999")), + + RadioItem.new("springletter") + .setMiscInfo(RadioMiscInfo.new().bSplashSong()) + .addItemInfo(RadioMetadata.newLA("Spring Letter", "90sFlav", RadioMetadata.createBandcampURI("spring-letter", "90sflav")).bOfficial()) + .addSource(RadioSource.new("audio/mp3", [ "mp3" ]) + .setURI("x-gwm://wiki/spring_letter.mp3") + .addPreview("mxc://glowers.club/wTLwzrymoDeNGdJLEpDoEBtZ", "image/jpg", "500x500")), + + RadioItem.new("weep") + .setMiscInfo(RadioMiscInfo.new().bSplashSong()) + .addItemInfo(RadioMetadata.newLA("Weep", "90sFlav", RadioMetadata.createBandcampURI("weep", "90sflav")).bOfficial()) + .addSource(RadioSource.new("audio/mp3", [ "mp3" ]) + .setURI("x-gwm://wiki/weep.mp3") + .addPreview("mxc://glowers.club/jIkXGOSECsPPyJTvgrxPhRoq", "image/jpg", "500x500")), + + RadioItem.new("whenimetyou") + .setMiscInfo(RadioMiscInfo.new().bSplashSong()) + .addItemInfo(RadioMetadata.newLA("When I Met You", "90sFlav", RadioMetadata.createBandcampURI("when-i-met-you", "90sflav")).bOfficial()) + .addSource(RadioSource.new("audio/mp3", [ "mp3" ]) + .setURI("x-gwm://wiki/when_i_met_you.mp3") + .addPreview("mxc://glowers.club/ntDKDFtqeQCmIVoRtzRQdvEC", "image/jpg", "500x500")), + + RadioItem.new("sevenofnine") + .setMiscInfo(RadioMiscInfo.new().bSplashSong()) + .addItemInfo(RadioMetadata.newLA("Seven of Nine", "90sFlav", RadioMetadata.createBandcampURI("seven-of-nine", "90sflav")).bOfficial()) + .addSource(RadioSource.new("audio/mp3", [ "mp3" ]) + .setURI("x-gwm://wiki/seven_of_nine.mp3") + .addPreview("mxc://glowers.club/kDNCBjvQxfPvtJVmfEMmcdov", "image/jpg", "999x999")), + + RadioItem.new("callme") + .setMiscInfo(RadioMiscInfo.new().bSplashSong()) + .addItemInfo(RadioMetadata.newLA("Call Me", "90sFlav", RadioMetadata.createBandcampURI("call-me", "90sflav")).bOfficial()) + .addSource(RadioSource.new("audio/mp3", [ "mp3" ]) + .setURI("x-gwm://wiki/call_me.mp3") + .addPreview("mxc://glowers.club/kDNCBjvQxfPvtJVmfEMmcdov", "image/jpg", "999x999")), + + RadioItem.new("midnightsession") + .setMiscInfo(RadioMiscInfo.new().bSplashSong()) + .addItemInfo(RadioMetadata.newLA("Midnight Session", "90sFlav", RadioMetadata.createBandcampURI("-", "90sflav")).bOfficial()) + .addSource(RadioSource.new("audio/mp3", [ "mp3" ]) + .setURI("x-gwm://wiki/полуночная_сессия.mp3") + .addPreview("mxc://glowers.club/pLexnITqTOhJUXBOKtDAiZzv", "image/jpg", "288x288")), + + RadioItem.new("southdakota") + .setMiscInfo(RadioMiscInfo.new().bSplashSong()) + .addItemInfo(RadioMetadata.newLA("South Dakota", "Keeloh", RadioMetadata.createSoundcloudURI("southdakota", "keelohproducer")).bOfficial()) + .addSource(RadioSource.new("audio/mp3", [ "mp3" ]) + .setURI("x-gwm://wiki/southdakota.mp3") + .addPreview("mxc://glowers.club/FJfFhscJavurBvoEuRbykpmM", "image/jpg", "500x500")), + + RadioItem.new("dobson") + .addItemInfo(RadioMetadata.newLA("Without Me", "Eminem", RadioMetadata.createYouTubeURI("YVkUvmDQ3HY")).bOriginal().bOfficial()) + .addItemInfo(RadioMetadata.newLA(null, null, RadioMetadata.createAppleMusicURI(15477279)).bOriginal().bOfficial().bAlt()) + .addSource(RadioSource.new("video/mp4", [ "avc1.64000D", "mp4a.40.2" ]) + .setURI("mxc://glowers.club/mRtnfcoHYkyAOXSkYvrnbZgX") + .addPreview("mxc://glowers.club/eHKVfIITgdRKERhVOPSTDQAc", "image/jpg", "432x426")), + + // FIXME: Original audio source found, not remix source or video source + RadioItem.new("poljacked") + //.setMetadata("BLACK BLADE", "Seyit Akbas", "https://youtu.be/6RnNXLZ2rfw", "1.15%, pitched up") + .addItemInfo(RadioMetadata.newLV(null, null, RadioMetadata.createBooruSoyURI(23224))) + .addItemInfo(RadioMetadata.newLA("NEON BLADE", "MoonDeity", RadioMetadata.createYouTubeURI("Mu965dWgMMQ"), "1.315%, pitched up, bass boosted").bOriginal().bOfficial()) + .addItemInfo(RadioMetadata.newLA(null, null, RadioMetadata.createAppleMusicURI(1627535463)).bOriginal().bOfficial().bAlt()) + .addSource(RadioSource.new("video/mp4", [ "avc1.64001E", "mp4a.40.2" ]) + .setURI("mxc://glowers.club/stlNnyEIUVGluyhpPSGhtjJg") + .addPreview("mxc://glowers.club/BczfFtojBhwMxTNjoZfhemam", "image/jpg", "638x360") + .addPreview("mxc://glowers.club/wVLvTOIodRqOcasDAFxkqHaG", "video/mp4", [ "avc1.4D401F" ], "358x360", EDisplayType.SQUARE) + .addPreview("mxc://glowers.club/EBQRprvJmkNfsZTzCzDkPVTv", "image/gif", "320x320", EDisplayType.SQUARE)) + .addSource(RadioSource.new("video/webm", [ "vp8", "vorbis" ]) + .setURI("mxc://glowers.club/xnYQdrroEheIZBfigHGZferu") + .addPreview("mxc://glowers.club/BczfFtojBhwMxTNjoZfhemam", "image/jpg", "638x360") + .addPreview("mxc://glowers.club/wVLvTOIodRqOcasDAFxkqHaG", "video/mp4", [ "avc1.4D401F" ], "358x360", EDisplayType.SQUARE) + .addPreview("mxc://glowers.club/EBQRprvJmkNfsZTzCzDkPVTv", "image/gif", "320x320", EDisplayType.SQUARE)), + + RadioItem.new("femalecops") + .setMiscInfo(RadioMiscInfo.new().bAltMedia()) + .addSource(RadioSource.new("video/mp4", [ "avc1.64001F", "mp4a.40.2" ]) + .setURI("mxc://glowers.club/ZSyUyEGvOLOoUaqFVoCYPTif") + .addPreview("mxc://glowers.club/BVQhXkWSYbvFcSerzVZFgfrn", "image/jpg", "600x600")) + .addSource(RadioSource.new("video/webm", [ "vp9", "opus" ]) + .setURI("mxc://glowers.club/RYjIfTiZKSKBdYBvjGtyBILa") + .addPreview("mxc://glowers.club/BVQhXkWSYbvFcSerzVZFgfrn", "image/jpg", "600x600")), + + RadioItem.new("autisticclowns") + .setMiscInfo(RadioMiscInfo.new().bAltMedia()) + .addSource(RadioSource.new("video/mp4", [ "avc1.640015", "mp4a.40.2" ]) + .setURI("mxc://glowers.club/fGszYTUkpyWsKuxphzJSfAZk") + .addPreview("mxc://glowers.club/esDPBgpoTvTtqekHAQJRwqtK", "image/jpg", "480x272")) + .addSource(RadioSource.new("video/quicktime", [ "avc1.4D0015", "mp4a.40.2" ]) + .setURI("mxc://glowers.club/WqYtubpqVplLjSxxmaxNeQir") + .addPreview("mxc://glowers.club/esDPBgpoTvTtqekHAQJRwqtK", "image/jpg", "480x272")), + + RadioItem.new("ifuckinglovescience") + .setMiscInfo(RadioMiscInfo.new().bExplicit()) + .addItemInfo(RadioMetadata.newLV(null, null, RadioMetadata.createBooruSoyURI(20571))) + .addItemInfo(RadioMetadata.newLA("I FUCKING LOVE SCIENCE", "Hank Green", RadioMetadata.createYouTubeURI("RECuQaaGGfA")).bOfficial().bExplicit()) + .addItemInfo(RadioMetadata.newLA(null, null, RadioMetadata.createAppleMusicURI(868150396)).bOfficial().bExplicit().bAlt()) + .addSource(RadioSource.new("video/mp4", [ "avc1.4D4029", "mp4a.40.2" ]) + .setURI("mxc://glowers.club/tLnDwRvJDckOIopncgUfodAn") + .addPreview("mxc://glowers.club/burAaUYSveyoVMHrijAsAxlL", "image/jpg", "768x432")) + .addSource(RadioSource.new("video/webm", [ "vp9", "opus" ]) + .setURI("mxc://glowers.club/abgQasVrghOSkpRZTIfHMFyF") + .addPreview("mxc://glowers.club/IhivpcDssjnjSDHBdMEGGwVp", "image/jpg", "320x180")) + .addSource(RadioSource.new("audio/mp3", [ "mp3" ]) + .setURI("mxc://glowers.club/uoohwSBNmaXjTLZFwodmEwwr") + .setMetadata("I FUCKING LOVE SCIENCE", "Hank Green", "https://youtu.be/RECuQaaGGfA") + .addPreview("mxc://glowers.club/aAYxVtwfHuIqXzYkrVwHZDjl", "image/jpg", "500x500")), + + RadioItem.new("dootnukem") + .addItemInfo(RadioMetadata.newLA("Doot Nukem", "Sylva", RadioMetadata.createYouTubeURI("cHJQ6LqZ4ks")).bOfficial()) + .addItemInfo(RadioMetadata.newLA("Grabbag (Duke Nukem 3D Main Menu Theme)", "Lee Jackson", RadioMetadata.createYouTubeURI("GGt5PgednCo")).bOriginal()) + .setMetadata("Doot Nukem", "Sylva", "https://youtu.be/cHJQ6LqZ4ks") + .addSource(RadioSource.new("audio/mp3", [ "mp3" ]) + .setURI("mxc://glowers.club/xsuBJwyAUwuDoASbmqjZWrLO") + .addPreview("mxc://glowers.club/BDcGZnyBusBBpfbMkeoSAHWf", "image/jpg", "400x400")), + + // TODO: No video attribution + RadioItem.new("hyperborea") + .addItemInfo(RadioMetadata.newLA("Gotye - WokeUpLikeThis", "BLACCMASS", RadioMetadata.createYouTubeURI("TLsLd22mhOA")).bOriginal().bOfficial()) + .addSource(RadioSource.new("video/mp4", [ "avc1.4D401F", "mp4a.40.2" ]) + .setURI("mxc://glowers.club/egAyPBuGNFPYyvnMWMxySLiX") + .addPreview("mxc://glowers.club/dOxJvUWRlGqRRxJdpDRZyeko", "image/jpg", "640x360")), + + RadioItem.new("feizhou") + .addItemInfo(RadioMetadata.newLAV("没有共产党就没有新中国", "郝歌", RadioMetadata.createYouTubeURI("5tCMI0uKbBE"))) + .addItemInfo(RadioMetadata.newLA("没有共产党就没有新中国", "曹火星", RadioMetadata.createYouTubeURI("72tiPR12fLA")).bOriginal()) + .addSource(RadioSource.new("video/mp4", [ "avc1.42C015", "mp4a.40.2" ]) + .setURI("mxc://glowers.club/fVKJZgWpIHRmilhZLboFXOIK") + .addPreview("mxc://glowers.club/EuYQZnqLjRGPMocgqGqtKFpJ", "image/jpg", "366x240")) + .addSource(RadioSource.new("video/webm", [ "vp8", "vorbis" ]) + .setURI("mxc://glowers.club/EVRGpqeefZdsjVawOvSkOdvZ") + .addPreview("mxc://glowers.club/GywrCPMpcpsLFJQAuYoZRuzq", "image/jpg", "366x240")), + + RadioItem.new("breathe") + .setMetadata("Breathe (in the Air)", "Pink Floyd") + .addSource(RadioSource.new("video/mp4", [ "avc1.64001E", "mp4a.40.2" ]) + .setURI("mxc://glowers.club/MOyzIHtapiyrpxaYRoiQqMqf") + .addPreview("mxc://glowers.club/KytvUDmLWCFKQWwHemdDTksd", "image/jpg", "640x360")) + .addSource(RadioSource.new("video/webm", [ "vp8", "vorbis" ]) + .setURI("mxc://glowers.club/HAGTNySLRemaDfjZMSmpktDj") + .addPreview("mxc://glowers.club/KytvUDmLWCFKQWwHemdDTksd", "image/jpg", "640x360")), + + RadioItem.new("fuckjuice") + .setMetadata("Fuck Jannies and Fuck Juice", "The Crystal Crypt") + .addSource(RadioSource.new("audio/mp3", [ "mp3" ]) + .setURI("mxc://glowers.club/bxMoRJmYFteVNNntNhqrniNJ") + .addPreview("mxc://glowers.club/YgeFvRPLguZOCGtUJFJednNj", "image/jpg", "276x240")), + + RadioItem.new("onegame") + .setMetadata("ONE GAME", "Chris Voiceman") + .addSource(RadioSource.new("audio/mp3", [ "mp3" ]) + .setURI("mxc://glowers.club/iejQiyaVtnqhuSSCuXqviAIN") + .addPreview("mxc://glowers.club/bZonXMZjhadXyazpKscvRBiP", "image/jpg", "255x255")), + + RadioItem.new("imaginebeingdruckmann") + .setMetadata("Imagine Being Druckmann", "Chris Voiceman") + .addSource(RadioSource.new("audio/mp3", [ "mp3" ]) + .setURI("mxc://glowers.club/wRsCVvtUiLzFnDjwwgkCnXzN") + .addPreview("mxc://glowers.club/ipWDVreHeWKOqefUBuvyFxjj", "image/jpg", "255x255")), + + RadioItem.new("copeacabana") + .setMetadata("Cope-a-Cabana", "Chris Voiceman") + .addSource(RadioSource.new("audio/mp3", [ "mp3" ]) + .setURI("mxc://glowers.club/zEmMFUyaIBmuqAeogVZAMrmO") + .addPreview("mxc://glowers.club/WBhXYzFgTyMVTTEWKwzDfsgF", "image/jpg", "512x512")), + + RadioItem.new("jihad") + .setMetadata("Heyaw Rijal Al Qassam", "Inshad Ensemble") + .addStyleTags([ "artwork-box-no-shadow" ]) + .addSource(RadioSource.new("audio/mp3", [ "mp3" ]) + .setURI("x-gwm://gnujihad.mp3") + .addPreview("x-gwm://chromiumjihad.gif", "image/gif", "420x236")), + + RadioItem.new("thisistheinfowar") + .setMiscInfo(RadioMiscInfo.new().bMetadataPreferId()) + .setMetadata("In the House, In a Heartbeat", "John Murphy") + .addSource(RadioSource.new("video/mp4", [ "avc1.64001E", "mp4a.40.2" ]) + .setURI("mxc://glowers.club/HVZUugkUhbJEPDncuTpAnOfh") + .addPreview("mxc://glowers.club/klXuXEwYNmYvsZhNOaXsDGJq", "image/jpg", "640x360")) + .addSource(RadioSource.new("video/webm", [ "vp8", "vorbis" ]) + .setURI("mxc://glowers.club/znoCPlczXEXaEAVaiKHopnAV") + .addPreview("mxc://glowers.club/klXuXEwYNmYvsZhNOaXsDGJq", "image/jpg", "640x360")), + + RadioItem.new("floyd") + .setMiscInfo(RadioMiscInfo.new().bAltMedia()) + .setMetadata("Paralyzer", "Finger Seven") + .addSource(RadioSource.new("video/mp4", [ "avc1.640020", "mp4a.40.2" ]) + .setURI("mxc://glowers.club/qtBuqQBqBrpyusCsWYXlgmtE") + .addPreview("mxc://glowers.club/JjsGuJkEGAbudLaYvbAcxryQ", "image/jpg", "600x600")) + .addSource(RadioSource.new("video/quicktime", [ "avc1.640020", "mp4a.40.2" ]) + .setURI("mxc://glowers.club/IusHmBCOyJZFGHxsGjRkYqXf") + .addPreview("mxc://glowers.club/JjsGuJkEGAbudLaYvbAcxryQ", "image/jpg", "600x600")), + + RadioItem.new("amd") + .setMiscInfo(RadioMiscInfo.new().bAltMedia()) + .setMetadata("Svetovid", "Jan Janko Močnik") + .addSource(RadioSource.new("video/mp4", [ "avc1.64001F", "mp4a.40.2" ]) + .setURI("mxc://glowers.club/wgQPrdTCUUTnKgpnwDVlEakc") + .addPreview("mxc://glowers.club/GccquMupQGvDXYNeifUbxfuy", "image/jpg", "800x450")) + .addSource(RadioSource.new("video/webm", [ "vp8", "vorbis" ]) + .setURI("mxc://glowers.club/RBvSWrEWiFtIuDUCoxZbCDTW") + .addPreview("mxc://glowers.club/GccquMupQGvDXYNeifUbxfuy", "image/jpg", "800x450")), + + // TODO: Add thumbnail + RadioItem.new("israel") + .setMetadata("Shake Israel's Security", "National Radio") + .addSource(RadioSource.new("audio/mp3", [ "mp3" ]) + .setURI("mxc://glowers.club/cJMVPnPpkSZFHIRHtzXRrvna")), + + RadioItem.new("honorary") + .setMiscInfo(RadioMiscInfo.new().bMetadataPreferId()) + .setMetadata("Erika", "Major Han Friess") + .addSource(RadioSource.new("video/mp4", [ "avc1.64001E", "mp4a.40.2" ]) + .setURI("mxc://glowers.club/cQgzwWXtxDSqqOlwGZYShVXS") + .addPreview("mxc://glowers.club/aENkwWEKXIztqyBTJrBHCmRQ", "image/jpg", "800x450")) + .addSource(RadioSource.new("video/webm", [ "vp8", "vorbis" ]) + .setURI("mxc://glowers.club/qMkbHHunSgPzkFpcoOuYeIuE") + .addPreview("mxc://glowers.club/aENkwWEKXIztqyBTJrBHCmRQ", "image/jpg", "800x450")), + + RadioItem.new("bashar") + .setMetadata("God, Syria, and Bashar", "Rami Kazour") + .addSource(RadioSource.new("audio/mp3", [ "mp3" ]) + .setURI("mxc://glowers.club/egmaXMIHsNmblfDjyTGHdyas") + .addPreview("mxc://glowers.club/wpwHKoSFsxkNttaNyaNFzhMk", "image/jpg", "794x582")), + + RadioItem.new("hatethem") + .setMetadata("Niggerz Bop", "Mike David", "https://youtu.be/VkcAXS9IKdc") + .setNiggadata("hatethem", "585a286c042231244a73b19e") + .addSource(RadioSource.new("audio/mp3", [ "mp3" ]) + .setURI("mxc://glowers.club/kYKpgAelvgRjDOVHJYKIYwdS") + .addPreview("mxc://glowers.club/IbShqtSWZksFbnOdFiVDFwqD", "image/jpg", "609x600")) + .addSource(RadioSource.new("audio/ogg", [ "opus" ]) + .setURI("mxc://glowers.club/wdflEjUfqAkoyOyCDBXebDkw") + .addPreview("mxc://glowers.club/IbShqtSWZksFbnOdFiVDFwqD", "image/jpg", "609x600")), + + RadioItem.new("wishmaster") + .setMetadata("Wishmaster", "Van Canto", "https://youtu.be/XCGQiGEYl4Y") + .setNiggadata("wishmaster", "570550d318f3c6dc5677b9f6") + .addSource(RadioSource.new("audio/mp3", [ "mp3" ]) + .setURI("mxc://glowers.club/TWmjcXRxqaVOfYnWPItiPAcd") + .addPreview("mxc://glowers.club/OKTbfAlagYXulaeBmgwkNwii", "image/jpg", "352x288", EDisplayType.STRETCH)) + .addSource(RadioSource.new("audio/ogg", [ "vorbis" ]) + .setURI("mxc://glowers.club/uaxDKxCUtRVXMWAJgoKeiNcJ") + .addPreview("mxc://glowers.club/OKTbfAlagYXulaeBmgwkNwii", "image/jpg", "352x288", EDisplayType.STRETCH)), + + RadioItem.new("negromancy") + .setMiscInfo(RadioMiscInfo.new().bAltMedia()) + .setMetadata(undefined, undefined, "https://youtu.be/PxjA-jq1e7E") + .addSource(RadioSource.new("video/mp4", [ "avc1.64001E", "mp4a.40.2" ]) + .setURI("mxc://glowers.club/kYDmqwhymAidlTbdFHHliXrk") + .addPreview("mxc://glowers.club/qAtvGoVERlYfFnNWeizoYiGP", "image/jpg", "640x360")) + .addSource(RadioSource.new("video/webm", [ "vp9", "opus" ]) + .setURI("mxc://glowers.club/hRDjowdyaPRkdrRkHtCeqGug") + .addPreview("mxc://glowers.club/qAtvGoVERlYfFnNWeizoYiGP", "image/jpg", "640x360")), + + RadioItem.new("crocodilechop") + .setMetadata("Crocodile Chop", "Neil Cicierega", "https://soundcloud.com/neilcic/crocodile-chop") + .setNiggadata("crocodilechop", "570550d318f3c6dc5677b9d1") + .addSource(RadioSource.new("audio/mp3", [ "mp3" ]) + .setURI("mxc://glowers.club/CIwbaGcPmsbqvZtdILxJxdHN") + .addPreview("mxc://glowers.club/ITHVCToEENFZhXGKaYuHplFV", "image/jpg", "500x500")), + + RadioItem.new("tapeworms") + .setMetadata("I Staple Tapeworms on my Penis", "SillyJenny9000", "https://youtu.be/v21iDJeWfVE") + .setNiggadata("tapeworms", "570550d318f3c6dc5677b9b0") + .addSource(RadioSource.new("audio/mp3", [ "mp3" ]) + .setURI("mxc://glowers.club/NnrVJeWXXzqEUDhDQazMDYrr") + .addPreview("mxc://glowers.club/jKmUwOLJMGPAhdJGusuWCJgQ", "image/jpg", "652x619")), + + RadioItem.new("withoutcosby") + .setMetadata("without cosby", "bong iguana", "https://soundcloud.com/bong-iguana/without-cosby") + .setNiggadata("withoutcosby", "570550d318f3c6dc5677b9e9") + .addSource(RadioSource.new("audio/mp3", [ "mp3" ]) + .setURI("mxc://glowers.club/ERjkVBvKLvTIBEhOsEkWHENM") + .addPreview("mxc://glowers.club/xgoxdfvMRtmobnPMTrdqsQic", "image/jpg", "500x500")), + + RadioItem.new("niggatorial") + .setMiscInfo(RadioMiscInfo.new().bMetadataPreferId()) + .setMetadata("I'm The 2007 YouTube Tutorial", "▲A▲", "https://soundcloud.com/kraiqyttyj/im-the-2007-youtube-tutorial") + .setNiggadata("tutorial", "570550d318f3c6dc5677b9d7") + .addSource(RadioSource.new("audio/mp3", [ "mp3" ]) + .setURI("mxc://glowers.club/XZGliHHOkhcxCSUxKoIkgUlI") + .addPreview("mxc://glowers.club/nAaKRXuNBjltAYJbsLnjGusH", "image/jpg", "899x715")), + + RadioItem.new("imposterd") + .setMiscInfo(RadioMiscInfo.new().bSplashSong().bAmogusSong()) + .setMetadata("Among Us Eurobeat Remix", "maki ligon", "https://soundcloud.com/maki-ligon-deez-nutz/among-us-drip-eurobeat-remix") + .addStyleTags([ "background-noblur-nobgsize", "artwork-nobox", "artwork-box-no-shadow" ]) + .addSource(RadioSource.new("audio/mp3", [ "mp3" ]) + .setURI("x-gwm://include/imposterd.mp3") + .addPreview("x-gwm://include/crewmate.gif", "image/gif", "128x108")), + + RadioItem.new("dripmachine") + .setMiscInfo(RadioMiscInfo.new().bSplashSong().bAmogusSong()) + .setMetadata("Amogus Drip Machine", "maki ligon", "https://soundcloud.com/maki-ligon-deez-nutz/amogus-drip-machine") + .addSource(RadioSource.new("audio/mp3", [ "mp3" ]) + .setURI("mxc://glowers.club/DyboACFryZTCDeIgHtcRaBpL") + .addPreview("mxc://glowers.club/VFehbVZaCVcNhQTYWuWRYTiM", "image/jpg", "500x500")), + + RadioItem.new("loneimposter") + .setMiscInfo(RadioMiscInfo.new().bSplashSong().bAmogusSong()) + .setMetadata("Lone Impostor", "maki ligon", "https://youtu.be/QbIckU4sXBM") + .addSource(RadioSource.new("audio/mp3", [ "mp3" ]) + .setURI("mxc://glowers.club/oswdiINtTUUbQYEMEKpVmsHN") + .addPreview("mxc://glowers.club/jYlptPwURJXVMBNivxlBFPuP", "image/jpg", "1440x1440")), + + RadioItem.new("amogusdrip") + .setMiscInfo(RadioMiscInfo.new().bSplashSong().bAmogusSong()) + .setMetadata("Among Us Drip Theme Song", "Leonz", "https://youtu.be/grd-K33tOSM") + .addSource(RadioSource.new("audio/mp3", [ "mp3" ]) + .setURI("mxc://glowers.club/xQHxUgpRUMtIkiBaszXiGAvh") + .addPreview("mxc://glowers.club/erMHzidVOoqoWFXIxXAcozqW", "image/jpg", "512x512")), + + RadioItem.new("crewmate") + .setMiscInfo(RadioMiscInfo.new().bSplashSong().bAmogusSong()) + .setMetadata("Among Us (Lofi Hip Hop Remix)", "Leonz", "https://youtu.be/gU39w8s54_Q") + .addSource(RadioSource.new("audio/mp3", [ "mp3" ]) + .setURI("mxc://glowers.club/txMHzslGxRyDmNVsUCObJRzc") + .addPreview("mxc://glowers.club/PfEBpatFHatQubohkgQThOor", "image/jpg", "632x632")), + + RadioItem.new("GETOUTOFMYHEAD") + .setMiscInfo(RadioMiscInfo.new().bSplashSong().bAmogusSong()) + .setMetadata("GETOUTOFMYHEAD", "placeboing", "https://youtu.be/Xnv38FnLkbM") + .addSource(RadioSource.new("video/mp4", [ "avc1.4D401F", "mp4a.40.2" ]) + .setURI("mxc://glowers.club/sEtheeOyZlztUtQuHOzOJpOk") + .addPreview("mxc://glowers.club/bJveZSEIXFsnEKdjDvwmmENo", "image/jpg", "800x450")), + + RadioItem.new("gay") + .setMetadata("I Am Gay", "Holland Boys", "https://youtu.be/cZs_nP1WufE") + .addTags([ "gay" ]) + .addSource(RadioSource.new("audio/mp3", [ "mp3" ]) + .setURI("mxc://glowers.club/FAaatrvlEjJFljvNHhDhcNGl") + .addPreview("mxc://glowers.club/KrefLQCybeCNwLIFZwzWVolF", "image/png", "489x512")), + + RadioItem.new("floydtrix") + .setMetadata("Clubbed to Death", "Rob Dougan", "https://youtu.be/pFS4zYWxzNA") + .addSource(RadioSource.new("audio/mp3", [ "mp3" ]) + .setURI("mxc://glowers.club/YUqRzRddNWuXQaAlPPMpwvse") + .addPreview("mxc://glowers.club/mFyOeCDaBRhxZCXeeMxDmZhD", "image/jpg", "588x634")), + + RadioItem.new("hotline") + .setMiscInfo(RadioMiscInfo.new().bAltMedia()) + .setMetadata("Gangster Party Line", "Brent Weinbach", "https://youtu.be/Cx1J2CzNnS8") + .setNiggadata("hotline", undefined) + .addSource(RadioSource.new("video/mp4", [ "avc1.4D401F", "mp4a.40.2" ]) + .setURI("mxc://glowers.club/gnZElBwskTnBXkeZPiINVtSB") + .addPreview("mxc://glowers.club/iPoGcvVbhmElZgaZuEdNQDBJ", "image/jpg", "480x356")), + + RadioItem.new("bldm") + .setMetadata("Black Lives (Don't) Matter", "Moonman") + .addTags([ "moonman" ]) + .addSource(RadioSource.new("audio/mp3", [ "mp3" ]) + .setURI("mxc://glowers.club/OXoYKzSBuKWHXcHDzZIaMfja") + .addPreview("mxc://glowers.club/SYLXjGHjuXzBnReqrfoRnuhg", "image/jpg", "1024x966")), + + RadioItem.new("amogusfloyd") + .setMiscInfo(RadioMiscInfo.new().bAltMedia()) + .setMetadata(undefined, undefined, "https://www.bitchute.com/video/PJXAu4xA4SUt/") + .addSource(RadioSource.new("audio/mp3", [ "mp3" ]) + .setURI("mxc://glowers.club/SzqNmhQOxaReaTmOegUItcHY") + .addPreview("mxc://glowers.club/aHWNIqMTUjXZRGZUqZCuOgbi", "image/jpg", "898x480", EDisplayType.LANDSCAPE)), + + RadioItem.new("whenblackissus") + .setMiscInfo(RadioMiscInfo.new().bAmogusSong().bMetadataPreferId()) + .setMetadata("Among Us theme song but it's in the style of Metallica's Black Album", "rex", "https://youtu.be/tA2Sr6GgbWo") + .addSource(RadioSource.new("audio/mp3", [ "mp3" ]) + .setURI("mxc://glowers.club/VDXDeYltzQOhTChQtmsZaybu") + .addPreview("mxc://glowers.club/JAWtHKTIefGJzYadLOUofaWl", "image/jpg", "1080x1080")), + + RadioItem.new("doot") + .setMetadata("Knee Deep in the Doot", "Nick Ino", "https://youtu.be/hzPpWInAiOg") + .addSource(RadioSource.new("audio/mp3", [ "mp3" ]) + .setURI("mxc://glowers.club/HvYByMgvFKONqIPmujCGaIIp") + .addPreview("mxc://glowers.club/dXELpQnsrrcwHuElSQHEPaRn", "image/jpg", "512x512")), + + RadioItem.new("ywnbaw") + .setMiscInfo(RadioMiscInfo.new().bAltMedia().bMetadataPreferId()) + .setMetadata("Professor proves SICKS are the best weapons", "Shadiversity", "https://youtu.be/mPnscZFUuog") + .addSource(RadioSource.new("video/mp4", [ "avc1.4D401F", "mp4a.40.2" ]) + .setURI("mxc://glowers.club/qYCbeFblMeBUMDZEiuBWuFnX") + .addPreview("mxc://glowers.club/dDbuzhpAoewNQoKHCejeQqdk", "image/jpg", "800x450")), + + RadioItem.new("feedandseed") + .setMiscInfo(RadioMiscInfo.new().bAltMedia()) + .addSource(RadioSource.new("video/mp4", [ "avc1.64001E", "mp4a.40.2" ]) + .setURI("mxc://glowers.club/vsMepElCpNKFgLltczQRjxfJ") + .addPreview("mxc://glowers.club/zUXbbLOCCxZoZDRIwVCzHFYC", "image/jpg", "654x480")), + + RadioItem.new("yatta") + .setMetadata("YATTA!", "HAPPATAI!", "https://youtu.be/rW6M8D41ZWU") + .setNiggadata("yatta", "570550d318f3c6dc5677b9bc") + .addSource(RadioSource.new("audio/mp3", [ "mp3" ]) + .setURI("mxc://glowers.club/oKXrDvGUNQJWzBsmwYwjsCZs") + .addPreview("mxc://glowers.club/dBJtIlaEcsZoflXYqPnihooO", "image/jpg", "374x374")), + + RadioItem.new("alexjones") + .setMiscInfo(RadioMiscInfo.new().bMetadataPreferId()) + .setMetadata("Alex Jones Remix: Renai Circulation", "Triple-Q", "https://youtu.be/ODZE5peUfWQ") + .setNiggadata("alexjones", "5796ca3646f17bf005a01be3") + .addSource(RadioSource.new("video/mp4", [ "avc1.4D401F", "mp4a.40.2" ]) + .setURI("mxc://glowers.club/vADMHcSVAzikHdjSRXKsmllx") + .addPreview("mxc://glowers.club/igLcLQjIYWSzTCRAPDMxSAmh", "image/jpg", "853x480") + .addPreview("mxc://glowers.club/dcqgSxtEyYBQNpYLfpXkxADL", "image/jpg", "500x500", EDisplayType.SQUARE)), + + RadioItem.new("avemaria") + .setMetadata("Ave Maria", "Daniel Perret", "https://youtu.be/4swAeQGBvAs") + .addSource(RadioSource.new("audio/mp3", [ "mp3" ]) + .setURI("x-gwm://wiki/avemaria.mp3") + .addPreview("mxc://glowers.club/UvQPlQTYoDjufDtdjZnPShmm", "image/png", "546x546")), + + RadioItem.new("thelinuxexperience") + .setMiscInfo(RadioMiscInfo.new().bAltMedia()) + .addSource(RadioSource.new("video/mp4", [ "avc1.64001F", "mp4a.40.2" ]) + .setURI("mxc://glowers.club/kzIdHQbGtQIdEitOruQHrXnM") + .addPreview("mxc://glowers.club/wKPEYlKdaZiSdCDbImBInvxm", "image/jpg", "936x558")), + + RadioItem.new("mywifesblackson") + .setMetadata("My Wife's Black Son", "Morrakiu", "https://youtu.be/7NpfbX5coqA") + .addSource(RadioSource.new("video/mp4", [ "avc1.4D401F", "mp4a.40.2" ]) + .setURI("mxc://glowers.club/aJsmUUQuUDQMVbtKfagCjFQi") + .addPreview("mxc://glowers.club/JbyoUSTYQcQaLbEspdRcDhpM", "image/jpg", "936x558")) + .addSource(RadioSource.new("audio/mp3", [ "mp3" ]) + .setURI("mxc://glowers.club/aQgNmIbxNluQUxtMXBHMScQK") + .addPreview("mxc://glowers.club/yTuLtqURfBNcQdUOzDYrndAa", "image/jpg", "720x720", EDisplayType.SQUARE)), + + RadioItem.new("papermoms") + .setMetadata("paper moms", "DOGGIFY", "https://youtu.be/J1rREd1Mx30") + .addSource(RadioSource.new("audio/mp3", [ "mp3" ]) + .setURI("mxc://glowers.club/roFLNfJtjRcySTTKorwTnUXE") + .addPreview("mxc://glowers.club/YOWZcLaAVpfhvbJWkPIviEJG", "image/jpg", "500x500")), + + RadioItem.new("dngyamom") + .setMiscInfo(RadioMiscInfo.new().bMetadataPreferId()) + .setMetadata("Dynamite by BTS but it's Doin' Your Mom", "DOGGIFY", "https://youtu.be/P5UHstVXfTo") + .addSource(RadioSource.new("audio/mp3", [ "mp3" ]) + .setURI("mxc://glowers.club/hYvOhMKYzPMDsFuxLAmRzgyt") + .addPreview("mxc://glowers.club/jJdYBFoiPzJIApBvxsPtgkiy", "image/jpg", "500x500")), + + RadioItem.new("doinurmom") + .setMetadata("around your mom", "DOGGIFY", "https://youtu.be/HOZaPGzUVfE") + .addSource(RadioSource.new("audio/mp3", [ "mp3" ]) + .setURI("mxc://glowers.club/IDOxAAnfqKDqaPwRRHTOCfiG") + .addPreview("mxc://glowers.club/TrqWMwgxFtAZYkwmwzzAQzwa", "image/jpg", "500x500")), + + RadioItem.new("morshufugue") + .setMetadata("Morshu Fugue", "DOGGIFY", "https://youtu.be/jxEA1rdQacQ") + .addSource(RadioSource.new("audio/mp3", [ "mp3" ]) + .setURI("mxc://glowers.club/zPHRyVPopUarJWZrZVdKRZfu") + .addPreview("mxc://glowers.club/aaRfbfVYHCmYGzGIGMpGOFvJ", "image/jpg", "500x500")), + + RadioItem.new("letsgetittoya") + .setMetadata("Let's Get It To Ya", "DOGGIFY", "https://youtu.be/04f5Kb2iznA") + .addSource(RadioSource.new("audio/mp3", [ "mp3" ]) + .setURI("mxc://glowers.club/rWlzEKkPIxxpkRPDnouLijPm") + .addPreview("mxc://glowers.club/WFtHpbtgVSNjqnfwZpvwaPgo", "image/jpg", "500x500")), + + RadioItem.new("indialexjones") + .setMetadata("INDIALEX JONES", "LENNOZ", "https://youtu.be/Vhzzt4SK7zw") + .addSource(RadioSource.new("video/mp4", [ "avc1.4D401F", "mp4a.40.2" ]) + .setURI("mxc://glowers.club/abqGBuacMfwsfTBBONIyPpGt") + .addPreview("mxc://glowers.club/ScZuHjnVjYmIWYTuoTMsYnPh", "image/jpg", "853x480") + .addPreview("mxc://glowers.club/GiGkhVoVXtPHWYXrTICNDOho", "image/jpg", "500x500", EDisplayType.SQUARE)) + .addSource(RadioSource.new("audio/mp3", [ "mp3" ]) + .setURI("mxc://glowers.club/xYyAOUmNYGLmLQGtLalyaawr") + .addPreview("mxc://glowers.club/GiGkhVoVXtPHWYXrTICNDOho", "image/jpg", "500x500", EDisplayType.SQUARE)), + + RadioItem.new("nothinwrongwithme") + .setMetadata("nothin wrong with me", "LENNOZ", "https://soundcloud.com/mistalenoz/nothin-wrong-with-me") + .addSource(RadioSource.new("audio/mp3", [ "mp3" ]) + .setURI("mxc://glowers.club/wCaKTMGjInkxJZlDHOvmVvQj") + .addPreview("mxc://glowers.club/LyHkbBYYHEfWZaDqDSLcJhFG", "image/jpg", "500x500")), + + RadioItem.new("cutecute") + .setMetadata("cutecute", "LENNOZ", "https://soundcloud.com/mistalenoz/cutecute") + .addSource(RadioSource.new("audio/mp3", [ "mp3" ]) + .setURI("mxc://glowers.club/yBTztNIxTznUNQiXyNlVEOwW") + .addPreview("mxc://glowers.club/QtptSXlBkUgsKYIhcksFFZwn", "image/jpg", "500x500")), + + RadioItem.new("humbletower") + .setMetadata("Humble Tower", "LENNOZ", "https://soundcloud.com/mistalenoz/humble-tower") + .addSource(RadioSource.new("audio/mp3", [ "mp3" ]) + .setURI("mxc://glowers.club/hovFlQXesRfzhwuvXauxuvYj") + .addPreview("mxc://glowers.club/hRwuxtUsNhXPuQLITioKvbTt", "image/jpg", "500x500")), + + // FIXME: Dead source, highest quality was nigge.rs + RadioItem.new("lowbrawl") + .setMetadata("How Low Can You Brawl", "DatNiggaOvaDer", "https://soundcloud.com/datniggaovader/how-low-can-you-brawl") + .setNiggadata("lowbrawl", "570550d318f3c6dc5677b9c7") + .addSource(RadioSource.new("audio/mp3", [ "mp3" ]) + .setURI("mxc://glowers.club/JInznJPrTfiXUvQflZYAjsEt") + .addPreview("mxc://glowers.club/mieZMZQsCToujzcFnnsRMnfP", "image/jpg", "500x500")), + + RadioItem.new("smokeonme") + .setMetadata("Smoke On Me", "RedScreen", "https://soundcloud.com/alexmashup/smoke-on-me") + .setNiggadata("smokeonme", "570550d318f3c6dc5677b9c1") + .addSource(RadioSource.new("audio/mp3", [ "mp3" ]) + .setURI("mxc://glowers.club/YknxSPlNxSIoyAdRPhtxoJpM") + .addPreview("mxc://glowers.club/yjsnEeACeyLtDPiGNgVYEoKS", "image/jpg", "500x500")), + + RadioItem.new("slamtris") + .setMiscInfo(RadioMiscInfo.new().bMetadataPreferId()) + .setMetadata("Slamtris - Quad City DJs vs Hirokazu Tanaka", "comeonandslam", "https://youtu.be/TZv71qYVoJM") + .setNiggadata("slamtris", "570550d318f3c6dc5677b9a6") + .addSource(RadioSource.new("audio/mp3", [ "mp3" ]) + .setURI("mxc://glowers.club/bFBMRKTVpzrscifHWtiEIEQO") + .addPreview("mxc://glowers.club/SNTFtMEjYkqarTVlZYRxvFUn", "image/jpg", "500x500")), + + // TODO: Better artwork + RadioItem.new("selonlud") + .setMetadata("It's My Life", "Село і Люди", "https://youtu.be/d-VVnhwB4Gw") + .setNiggadata("selonlud", "570550d318f3c6dc5677b9a2") + .addSource(RadioSource.new("audio/mp3", [ "mp3" ]) + .setURI("mxc://glowers.club/QbPYwLTBToYdNySFTWqlPcYA") + .addPreview("mxc://glowers.club/leiVVHimwUdzqExdgGAenVjH", "image/jpg", "480x480")), + + RadioItem.new("hymn") + .setMetadata("TempleOS Hymn Risen (Remix)", "Dave Eddy", "https://youtu.be/IdYMA6hY_74") + .addSource(RadioSource.new("audio/mp3", [ "mp3" ]) + .setURI("mxc://glowers.club/xEceAHSVDIhMgZMEIOhJZlmS") + .addPreview("mxc://glowers.club/IQiMcCemNZIvtlbXIkxPyXty", "image/jpg", "720x720")), + + RadioItem.new("smokezelda") + .setMetadata("Play Zelda Everyday", "MC Fresh Dawg 3000", "https://soundcloud.com/mcfreshdawg3000/play-zelda-everyday") + .setNiggadata("smokezelda", "570550d318f3c6dc5677b9c2") + .addSource(RadioSource.new("audio/mp3", [ "mp3" ]) + .setURI("mxc://glowers.club/nGtHiLaIQgTLsmSGFqsuqkST") + .addPreview("mxc://glowers.club/BmEUsNOXaNKSrXdLRcVaLqoa", "image/jpg", "500x500")), + + RadioItem.new("katyusha") + .setMetadata("カチューシャ", "浜口史郎", "https://youtu.be/NGkXEGH0V1Y") + .setNiggadata("panzer", "570550d318f3c6dc5677b99a") + .addSource(RadioSource.new("audio/mp3", [ "mp3" ]) + .setURI("mxc://glowers.club/aJJKZCrBvRDcbrUQXoWiSLfL") + .addPreview("mxc://glowers.club/KGlWUXAnjOQOfnaGGaGMbdnz", "image/jpg", "500x500")), + + RadioItem.new("ohshitimfeelingit") + .setMetadata("OH SHIT I'M FEELING IT", "yung wenli", "https://soundcloud.com/yung_wenli/oh-shit-im-feeling-it") + .addSource(RadioSource.new("audio/mp3", [ "mp3" ]) + .setURI("mxc://glowers.club/OoDDxMutNpVzlTMbPiBfIvSp") + .addPreview("mxc://glowers.club/VrEtpPvDRBhlrxevPuIMAQLp", "image/jpg", "500x500")), + + RadioItem.new("killallthe") + .setMetadata("KILL ALL THE GAYS AND THE FAGGOTS", "Mike David", "https://archive.org/details/killallthegaysandthefaggots451904565") + .setNiggadata("killallthe", "570550d318f3c6dc5677ba01") + .addSource(RadioSource.new("audio/mp3", [ "mp3" ]) + .setURI("mxc://glowers.club/CbLUdYNkCDzYrgwzszgfQBsk") + .addPreview("mxc://glowers.club/qnAjqhGYzBLrEmlhzbxreoUC", "image/jpg", "500x500")), + + RadioItem.new("piebotnik") + .addItemInfo(RadioMetadata.newLAV("dr robotnik bakes a pie", "KnightOfGames", RadioMetadata.createYouTubeURI("jqKVLZ9wA24")).bOfficial()) + .addItemInfo(RadioMetadata.newLAV("In HoNoR oF oUr MoSt FuNnY tIkToK", "jaydencroes", RadioMetadata.createTikTokURI("6803451075920776454", "jaydencroes")).setFor(0).bOriginal().bOfficial()) + .addItemInfo(RadioMetadata.newLA("Dr. Robotnik's Theme", "中村正人", RadioMetadata.createYouTubeURI("KbhX7XKgkDU")).setFor(1).bOriginal()) + .addTags([ "ylyl" ]) + .addSource(RadioSource.new("video/mp4", [ "avc1.4D4028", "mp4a.40.2" ]) + .setURI("mxc://glowers.club/YUjleEpsnUnjnQPscgAzVPzX") + .addPreview("mxc://glowers.club/jJqCgRDjBVfCHfWzkNnFXuyF", "image/jpg", "800x450") + .addPreview("mxc://glowers.club/ZGGXHgsznHoqVrHotiPzpoGq", "image/jpg", "500x500", EDisplayType.SQUARE)) + .addSource(RadioSource.new("audio/mp3", [ "mp3" ]) + .setURI("mxc://glowers.club/JzNiDnSObgTxugOfEJOpHVHH") + .addPreview("mxc://glowers.club/ZGGXHgsznHoqVrHotiPzpoGq", "image/jpg", "500x500", EDisplayType.SQUARE)), + + RadioItem.new("donarudo") + .setMetadata("【ドナルド】もうランランルーしか聞こえない【東方】※立体視可能", "Hoshi-Kun", "https://www.nicovideo.jp/watch/sm3179171") + .addSource(RadioSource.new("video/mp4", [ "avc1.4D4029", "mp4a.40.2" ]) + .setURI("mxc://glowers.club/XNhgwibiPmHJARjWafsNhSlq", "v") + .addPreview("mxc://glowers.club/XwEnNcCEcufMhpmDzxJARYgY", "image/jpg", "853x480") + .addPreview("mxc://glowers.club/NBCRNrPnlJmUwiyCTAvKKkzd", "video/mp4", [ "avc1.4D4029" ], "384x384", EDisplayType.SQUARE) + .addPreview("mxc://glowers.club/WBuCuaxhOHahCzgSHrDBnyrV", "image/gif", "384x384", EDisplayType.SQUARE)) + .addSource(RadioSource.new("audio/mp3", [ "mp3" ]) + .setURI("mxc://glowers.club/CIfeVQBVYpBqZRxdVVJWfNJf") + .addPreview("mxc://glowers.club/NBCRNrPnlJmUwiyCTAvKKkzd", "video/mp4", [ "avc1.4D4029" ], "384x384", EDisplayType.SQUARE) + .addPreview("mxc://glowers.club/WBuCuaxhOHahCzgSHrDBnyrV", "image/gif", "384x384", EDisplayType.SQUARE)), + + RadioItem.new("elonmusk") + .setMetadata("Elon Musk", "Kirino Kōsaka", "https://soundcloud.com/kirinokosaka/elon-musk") + .addSource(RadioSource.new("audio/mp3", [ "mp3" ]) + .setURI("mxc://glowers.club/UeZJLHulmquZpzVMDLZJVEHu") + .addPreview("mxc://glowers.club/BCMoaoUANfsbKttHbjbKxHyS", "image/jpg", "500x500")), + + // TODO: Artwork could be better + RadioItem.new("greenday") + .setMetadata("That Hip New Meme Those Boys Are Talking About Today", "Dabunky", "https://soundcloud.com/dabunky/that-hip-new-meme-those-boys-are-talking-about-today") + .addSource(RadioSource.new("audio/mp3", [ "mp3" ]) + .setURI("mxc://glowers.club/ADNlzFLkpvlfUmGULvOfwVLH") + .addPreview("mxc://glowers.club/RhnfTvhvuQNqbqQSZDCYDhOf", "image/jpg", "500x500")), + + RadioItem.new("heavengrips") + .setMetadata("Rhythm Heaven Grips", "Kirino Kōsaka", "https://soundcloud.com/kirinokosaka/rhythm-heaven-grips") + .addSource(RadioSource.new("audio/mp3", [ "mp3" ]) + .setURI("mxc://glowers.club/fDmHjgmrYQcYjIiMIwAjUelF") + .addPreview("mxc://glowers.club/KIqvgGfUnrjGhhapMEWahlHM", "image/jpg", "500x500")), + + // TODO: Artwork could be better. White bordering + RadioItem.new("gangnam") + .setMetadata("now thats what i call murder", "Kirino Kōsaka", "https://soundcloud.com/kirinokosaka/ok-pls-stop") + .addSource(RadioSource.new("audio/mp3", [ "mp3" ]) + .setURI("mxc://glowers.club/qfHznoOiNjEfBjwWzgZJarGL") + .addPreview("mxc://glowers.club/msHrqYpLIReBYqWtcGrizMZy", "image/jpg", "500x500")), + + RadioItem.new("wearheadphones") + .setMetadata("I made this 2 months too late", "~Equalizee", "https://soundcloud.com/djequalizee/i-made-this-2-months-too-late") + .addSource(RadioSource.new("audio/mp3", [ "mp3" ]) + .setURI("mxc://glowers.club/WAPutdfyEeQWvgumweMUMkmx") + .addPreview("mxc://glowers.club/BqLFkwPPXOmpuGapmwBKQYWx", "image/jpg", "500x500")), + + // FIXME: Source is reupload, not original + RadioItem.new("gnuquest") + .setMetadata("GNU/STALLMAN QUEST", undefined, "https://youtu.be/Dn8gealMDsg") + .addSource(RadioSource.new("video/webm", [ "vp9", "vorbis" ]) + .setURI("mxc://glowers.club/PDbLjvNYeRTEpHMnYiGNFNBu") + .addPreview("mxc://glowers.club/BTiOylzdZcWrrxLGuCpQYcMK", "image/jpg", "562x312")) + .addSource(RadioSource.new("video/mp4", [ "avc1.4D4029", "mp4a.40.2" ]) + .setURI("mxc://glowers.club/yVvqUBGrZPxzIylwGXBsZtlw") + .addPreview("mxc://glowers.club/BTiOylzdZcWrrxLGuCpQYcMK", "image/jpg", "562x312")) + .addSource(RadioSource.new("audio/ogg", [ "vorbis" ]) + .setURI("mxc://glowers.club/hsjIhOxVpcHwlaHbmfSKmmrI") + .addPreview("mxc://glowers.club/gGvSqctfUJWHEYxYfbZEwQpn", "image/jpg", "416x416")) + .addSource(RadioSource.new("audio/mp3", [ "mp3" ]) + .setURI("mxc://glowers.club/fJQxNXitNpwnFpnLRWoVBoFN") + .addPreview("mxc://glowers.club/gGvSqctfUJWHEYxYfbZEwQpn", "image/jpg", "416x416")), + + RadioItem.new("wiphop") + .setMiscInfo(RadioMiscInfo.new().bSplashSong()) + .addItemInfo(RadioMetadata.newLA("[SP] WIP HOP", "CodeZombie", RadioMetadata.createSoundcloudURI("wip-hop", "codezombie")).bOfficial()) + .addSource(RadioSource.new("audio/mp3", [ "mp3" ]) + .setURI("mxc://glowers.club/CCsidQsdYksctdyCnlvfTDxx") + .addPreview("mxc://glowers.club/jJkVfsELEmptqtsJipFpmwxO", "image/jpg", "500x500")), + + RadioItem.new("lofibeatstorelaxandoppto") + .setMetadata("Gangnam Lofi", "Jayden Greig", "https://triple-q.bandcamp.com/track/gangnam-lofi") + .addItemInfo(RadioMetadata.newLA("Gangnam Style", "PSY", RadioMetadata.createYouTubeURI("9bZkp7q19f0")).setFor(0).bOriginal().bOfficial()) + .addItemInfo(RadioMetadata.newLA(null, null, RadioMetadata.createAppleMusicURI(1445144527)).setFor(0).bOriginal().bOfficial().bAlt()) + .addSource(RadioSource.new("audio/mp3", [ "mp3" ]) + .setURI("mxc://glowers.club/tYDWGHZGOPOFvKWBRNwCtPpJ") + .addPreview("mxc://glowers.club/JQaLfBTNmuiHfszGxsvgqSHI", "image/jpg", "500x500")), + + RadioItem.new("hansolo") + .setMetadata("I'm Han Solo", "Ben Afflack", "https://soundcloud.com/kevinafflack/im-han-solo") + .addSource(RadioSource.new("audio/mp3", [ "mp3" ]) + .setURI("mxc://glowers.club/BEZIgyGkIhVFiWzgCQADPDIT") + .addPreview("mxc://glowers.club/PiuHxoREZUkjstGdOPcbLwpr", "image/jpg", "500x500")), + + RadioItem.new("therhythmrises") + .setMetadata("Rhythm Heaven - Bane Interview", "Brocartoon", "https://youtu.be/9S3YXXp6bOM") + .addSource(RadioSource.new("video/mp4", [ "avc1.4D4029", "mp4a.40.2" ]) + .setURI("mxc://glowers.club/PNHAYakhyOXJTztenoQsALeN") + .addPreview("mxc://glowers.club/YVGalEuZMXeqBfxnBjVAqHHF", "image/jpg", "800x450")) + .addSource(RadioSource.new("audio/mp3", [ "mp3" ]) + .setURI("mxc://glowers.club/rBFanZsHSDRBfnsAACphydqD") + .addPreview("mxc://glowers.club/mBDjCbTFATCwsHKuPpGHFuiL", "image/jpg", "500x500")), + + // FIXME: No source + RadioItem.new("penis") + .addSource(RadioSource.new("audio/mp3", [ "mp3" ]) + .setURI("mxc://glowers.club/oWQQYoSSPiRwHMVPXsMDcCOb") + .addPreview("mxc://glowers.club/obrTGFVOQBuRhaZZNsmAvDuI", "image/jpg", "750x750")), + + // TODO: album art edit sucks lol + RadioItem.new("partyrockapple") + .addItemInfo(RadioMetadata.newLA("Party Rock Apple!!", "Triple-Q", RadioMetadata.createBandcampURI("party-rock-apple", "triple-q")).bOfficial()) + .addItemInfo(RadioMetadata.newLA("Party Rock Anthem", "LMFAO", RadioMetadata.createYouTubeURI("KQ6zr6kCPj8")).setFor(0).bOriginal().bOfficial()) + .addItemInfo(RadioMetadata.newLA(null, null, RadioMetadata.createAppleMusicURI(1440636627)).setFor(0).bOriginal().bOfficial().bAlt()) + .addItemInfo(RadioMetadata.newLA("Bad Apple!!", "nomico", RadioMetadata.createNicoNicoURI("sm8628149")).setFor(1).bOriginal()) + .addItemInfo(RadioMetadata.newLA(null, null, RadioMetadata.createAppleMusicURI(1440636627)).setFor(1).bOriginal().bOfficial().bAlt()) + .addSource(RadioSource.new("audio/mp3", [ "mp3" ]) + .setURI("mxc://glowers.club/ngNYSFAkLNDIMwlGsnVmwDpS") + .addPreview("mxc://glowers.club/QemTpdTrUYfKEoaWgrkUChIz", "image/jpg", "500x500")), + + RadioItem.new("fnaf") + .setMetadata("gangnam style joke", "Kirino Kōsaka", "https://soundcloud.com/kirinokosaka/gangnam-style-joke-1") + .addItemInfo(RadioMetadata.newLA("gangnam style joke", "Kirino Kōsaka", RadioMetadata.createSoundcloudURI("gangnam-style-joke-1", "kirinokosaka")).bOfficial()) + .addItemInfo(RadioMetadata.newLA("Les Toreadors Carmen", "Scott Cawthon", RadioMetadata.createYouTubeURI("GZOOx40rE3k")).setFor(0).bOriginal()) + .addItemInfo(RadioMetadata.newLA("Gas Pedal (feat. Iamsu!)", "Sage The Gemini", RadioMetadata.createYouTubeURI("X8LUd51IuiA")).setFor(1).bOriginal().bOfficial()) + .addItemInfo(RadioMetadata.newLA(null, null, RadioMetadata.createAppleMusicURI(1434900852)).setFor(1).bOriginal().bOfficial().bAlt()) + .addSource(RadioSource.new("audio/mp3", [ "mp3" ]) + .setURI("mxc://glowers.club/bmzxkzwFHChruYIjszvDrdRj") + .addPreview("mxc://glowers.club/TkvoCbwQQQAwOzKwaFARDmRH", "image/jpg", "500x500")), + + RadioItem.new("mail") + .addItemInfo(RadioMetadata.newLA("AOLNATION - Mail", "Personal Pong", RadioMetadata.createSoundcloudURI("aolnation-mail", "personalpong")).bOfficial()) + .addItemInfo(RadioMetadata.newLA("Sail", "AWOLNATION", RadioMetadata.createYouTubeURI("tgIqecROs5M")).bOriginal().bOfficial()) + .addItemInfo(RadioMetadata.newLA(null, null, RadioMetadata.createAppleMusicURI(422478211)).bOriginal().bOfficial().bAlt()) + .addSource(RadioSource.new("audio/mp3", [ "mp3" ]) + .setURI("mxc://glowers.club/HEDaNdMsVFzIgYDkymkxnhob") + .setPlaybackInfo(RadioPlaybackInfo.new().setPlaybackVolume(.4)) + .addPreview("mxc://glowers.club/vBvnVioYcHeIGtvHRAaWWCab", "image/jpg", "500x500")), + + RadioItem.new("thisislazytown") + .addItemInfo(RadioMetadata.newLA("This Is Lazy Town", "Soundclown Crimes Against Humanity", RadioMetadata.createSoundcloudURI("this-is-lazy-town", "thesoundclowncriminal"))) + .addItemInfo(RadioMetadata.newLA("We Are Number One", "Stefán Karl Stefánsson", RadioMetadata.createYouTubeURI("PfYnvDL0Qcw")).setFor(0).bOriginal().bOfficial()) + .addItemInfo(RadioMetadata.newLA(null, null, RadioMetadata.createAppleMusicURI(1650060971)).setFor(0).bOriginal().bOfficial().bAlt()) + .addItemInfo(RadioMetadata.newLA("This Is America", "Childish Gambino", RadioMetadata.createYouTubeURI("VYOjWnS4cMY")).setFor(1).bOriginal().bOfficial()) + .addItemInfo(RadioMetadata.newLA(null, null, RadioMetadata.createAppleMusicURI(1379046390)).setFor(1).bOriginal().bOfficial().bAlt()) + .addSource(RadioSource.new("audio/mp3", [ "mp3" ]) + .setURI("mxc://glowers.club/XKaBjJLglqeAgkkPgEclMBIS") + .addPreview("mxc://glowers.club/KcFqpISqthZSOFhntXAhkWIr", "image/jpg", "500x500")), + + // FIXME: No official source for Al-Sawarim. Artist listed on Apple Music but songs are unavailable in Europe + RadioItem.new("isisman") + .addItemInfo(RadioMetadata.newLA("ISISman", "Triple-Q", RadioMetadata.createBandcampURI("isisman", "triple-q")).bOfficial()) + .addItemInfo(RadioMetadata.newLA("Gentleman", "PSY", RadioMetadata.createYouTubeURI("ASO_zypdnsQ")).setFor(0).bOriginal().bOfficial()) + .addItemInfo(RadioMetadata.newLA(null, null, RadioMetadata.createAppleMusicURI(1452841625)).setFor(0).bOriginal().bOfficial().bAlt()) + .addItemInfo(RadioMetadata.newLA("Saleel al-Sawarim", "Abu Yasir", RadioMetadata.createInternetArchiveURI("saleel_al-sawarim")).setFor(1).bOriginal()) + .addSource(RadioSource.new("audio/mp3", [ "mp3" ]) + .setURI("mxc://glowers.club/pvVRMPsnBzCadKCSaZdMmluy") + .addPreview("mxc://glowers.club/zYqsUaZgAmqNDWymTPhdGJRi", "image/jpg", "500x500")), + + RadioItem.new("takyonmachine") + .setMiscInfo(RadioMiscInfo.new().bMetadataPreferId()) + .addItemInfo(RadioMetadata.newLAV("Wintergrips - Takyon Machine (music instrument using a very angry man)", "Uncle Ned", RadioMetadata.createYouTubeURI("gSQyPdYz4f4")).bOfficial()) + .addItemInfo(RadioMetadata.newLA("Marble Machine", "Wintergatan", RadioMetadata.createYouTubeURI("IvUU8joBb1Q")).setFor(0).bOriginal().bOfficial()) + .addItemInfo(RadioMetadata.newLA(null, null, RadioMetadata.createAppleMusicURI(1319433713)).setFor(0).bOriginal().bOfficial().bAlt()) + .addItemInfo(RadioMetadata.newLA("Takyon (Death Yon)", "Death Grips", RadioMetadata.createYouTubeURI("Htl3XWUhUOM")).setFor(1).bOriginal().bOfficial()) + .addItemInfo(RadioMetadata.newLA(null, null, RadioMetadata.createInternetArchiveURI("DeathGrips-ExMilitaryMixtape")).setFor(1).bOriginal().bOfficial().bAlt()) + .addSource(RadioSource.new("video/mp4", [ "avc1.4D4029", "mp4a.40.2" ]) + .setURI("mxc://glowers.club/khtMKBJAxrJgOQclpdzHDTzp") + .addPreview("mxc://glowers.club/RqnJOOLKBNBlXnHOWziASTkN", "image/jpg", "800x450")) + .addSource(RadioSource.new("audio/mp3", [ "mp3" ]) + .setURI("mxc://glowers.club/zUxmXPHvOAXxMwxUsRpVBucq") + .addPreview("mxc://glowers.club/VcwimKRasxlYLJMqYtJpOJGi", "image/jpg", "500x500")), + + RadioItem.new("ghostpieght") + .addItemInfo(RadioMetadata.newLAV("Ghost Pieght", "uncrumpled", RadioMetadata.createYouTubeURI("SDQ-Vvdqa3s")).bOfficial()) + .addItemInfo(RadioMetadata.newLAV("In HoNoR oF oUr MoSt FuNnY tIkToK", "jaydencroes", RadioMetadata.createTikTokURI("6803451075920776454", "jaydencroes")).setFor(0).bOriginal().bOfficial()) + .addItemInfo(RadioMetadata.newLA("Ghost Fight", "Toby Fox", RadioMetadata.createBandcampURI("ghost-fight-2", "tobyfox")).setFor(1).bOriginal().bOfficial()) + .addItemInfo(RadioMetadata.newLA(null, null, RadioMetadata.createAppleMusicURI(1119807147)).setFor(1).bOriginal().bOfficial().bAlt()) + .addTags([ "ylyl" ]) + .addSource(RadioSource.new("video/mp4", [ "avc1.4D4029", "mp4a.40.2" ]) + .setURI("mxc://glowers.club/bVJrnYxFEUzZnebnClncWYxK") + .addPreview("mxc://glowers.club/jJFRlPmcGmjDhnYDziBAmytm", "image/jpg", "800x450")) + .addSource(RadioSource.new("audio/mp3", [ "mp3" ]) + .setURI("mxc://glowers.club/PQENpluityBQawDQxZlJidYD") + .addPreview("mxc://glowers.club/OcyarqkcuWucgkXOGtcgpJAa", "image/jpg", "250x250")), + + RadioItem.new("moonbase") + .setMiscInfo(RadioMiscInfo.new().bExplicit()) + .addItemInfo(RadioMetadata.newLA("MOONBASE BABY (ft. Hatsune Miku)", "KnightOfGames", RadioMetadata.createYouTubeURI("MIxowl5YuaQ")).bOfficial().bExplicit()) + .addItemInfo(RadioMetadata.newLA("INDUSTRY BABY", "Lil Nas X", RadioMetadata.createYouTubeURI("UTHLKHL_whs")).bOriginal().bOfficial().bExplicit()) + .addItemInfo(RadioMetadata.newLA(null, null, RadioMetadata.createAppleMusicURI(1576552565)).bOriginal().bOfficial().bExplicit().bAlt()) + .addSource(RadioSource.new("audio/mp3", [ "mp3" ]) + .setURI("mxc://glowers.club/AybVVfwhrAOCWTkGtMeIxkdB") + .addPreview("mxc://glowers.club/NuOKRVCYsxywWAsmRdoSVJfX", "image/jpg", "500x500")), + + 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")), + + RadioItem.new("stronger") + .addItemInfo(RadioMetadata.newLA("♂ Stonebank - How strong you are ♂", "duyui", RadioMetadata.createYouTubeURI("MIxowl5YuaQ")).bOfficial()) + .addItemInfo(RadioMetadata.newLA("Stronger", "Stonebank", RadioMetadata.createYouTubeURI("I1NuCWfYeYc")).bOriginal().bOfficial()) + .addItemInfo(RadioMetadata.newLA(null, null, RadioMetadata.createAppleMusicURI(951790852)).bOriginal().bOfficial().bAlt()) + .addTags([ "gachi" ]) + .addSource(RadioSource.new("audio/mp3", [ "mp3" ]) + .setURI("mxc://glowers.club/COBFDSucwknvESlGcXQthuMN") + .addPreview("mxc://glowers.club/QnxgSLfTrHUJLeqreUVmCQhO", "image/jpg", "500x500")), + + RadioItem.new("cbt") + .setMiscInfo(RadioMiscInfo.new().bExplicit().bMetadataPreferId()) + .addItemInfo(RadioMetadata.newLA("unfinished/abandoned ytpmv projects 2 (feb 2020 - dec 2022)", "KnightOfGames", RadioMetadata.createYouTubeURI("d8FT7CpYMgQ"), "0:00 to 1:59").bOfficial()) + .addItemInfo(RadioMetadata.newLA("One-Winged Angel (Final Fantasy VII)", "Nobuo Uematsu", RadioMetadata.createYouTubeURI("zkZGFNCS6iQ")).setFor(0).bOriginal().bOfficial()) + .addItemInfo(RadioMetadata.newLA(null, null, RadioMetadata.createAppleMusicURI(1439125559)).setFor(0).bOriginal().bOfficial().bAlt()) + .addItemInfo(RadioMetadata.newLA("CocknBallTorture.ogg", "Alabaster Constantine III", RadioMetadata.createWikipediaURI("File:CocknBallTorture.ogg")).setFor(1).bOriginal().bOfficial()) + .addSource(RadioSource.new("audio/mp3", [ "mp3" ]) + .setURI("mxc://glowers.club/wkZaRVKkHqBCHpjYVQzKvsZs") + .addPreview("mxc://glowers.club/KgrZIZJKvEnWPWnDukTKaRqR", "image/jpg", "500x500")), + + RadioItem.new("fu") + .setMiscInfo(RadioMiscInfo.new().bMetadataPreferId()) + .addItemInfo(RadioMetadata.newLAV("unfinished/abandoned ytpmv projects 2 (feb 2020 - dec 2022)", "KnightOfGames", RadioMetadata.createYouTubeURI("d8FT7CpYMgQ"), "3:07 to 3:27").bOfficial()) + .addItemInfo(RadioMetadata.newLAV("Darkwing Duck (Turbografx 16) - Angry Video Game Nerd (AVGN)", "Cinemassacre", RadioMetadata.createYouTubeURI("x3cewR_sMYI"), "10:40 to 10:54").setFor(0).bOriginal().bOfficial()) + .addItemInfo(RadioMetadata.newLA("Mtc", "S3RL", RadioMetadata.createYouTubeURI("bO-NaEj2dQ0")).setFor(1).bOriginal().bOfficial()) + .addItemInfo(RadioMetadata.newLA(null, null, RadioMetadata.createAppleMusicURI(500173662)).setFor(1).bOriginal().bOfficial().bAlt()) + .addSource(RadioSource.new("video/mp4", [ "avc1.4D4029", "mp4a.40.2" ]) + .setURI("mxc://glowers.club/BAGYQxmJGQdjKYnUfufHnvDc") + .addPreview("mxc://glowers.club/fBUMGKYdJHmjSvPkCLiWSAEG", "image/jpg", "800x450")) + .addSource(RadioSource.new("audio/mp3", [ "mp3" ]) + .setURI("mxc://glowers.club/PRaJyCRuCnseFufeIlFeherJ") + .addPreview("mxc://glowers.club/skHzOJjiHKtFrzeBeVUZnsfh", "image/jpg", "500x500")), + + RadioItem.new("diamondsword") + .setMiscInfo(RadioMiscInfo.new().bMetadataPreferId()) + .addItemInfo(RadioMetadata.newLAV("unfinished/abandoned ytpmv projects 2 (feb 2020 - dec 2022)", "KnightOfGames", RadioMetadata.createYouTubeURI("d8FT7CpYMgQ"), "3:30 to 3:57").bOfficial()) + .addItemInfo(RadioMetadata.newLAV("I Can Swing My Sword! (feat. Terabrite)", "Tobuscus", RadioMetadata.createYouTubeURI("eN7dYDYfvVg")).setFor(0).bOriginal().bOfficial()) + .addItemInfo(RadioMetadata.newLA(null, null, RadioMetadata.createAppleMusicURI(518379277)).setFor(0).bOriginal().bOfficial().bAlt()) + .addItemInfo(RadioMetadata.newLA("Track 26 (Plantation)", "天谷大輔", RadioMetadata.createYouTubeURI("aHtWKbR_P-c")).setFor(1).bOriginal()) + .addSource(RadioSource.new("video/mp4", [ "avc1.4D4029", "mp4a.40.2" ]) + .setURI("mxc://glowers.club/LpzkeoSVxfFBPYCcVIRawREk") + .addPreview("mxc://glowers.club/XBVPXnaargixijAaKLxFWKEc", "image/jpg", "800x454")) + .addSource(RadioSource.new("audio/mp3", [ "mp3" ]) + .setURI("mxc://glowers.club/qsxMSogWbxDJOTpWetgcyqfs") + .addPreview("mxc://glowers.club/vmwNdqdALTzZlcZgpgXzoBmN", "image/jpg", "500x500")), + + RadioItem.new("NIGGER") + .setMiscInfo(RadioMiscInfo.new().bExplicit()) + .addItemInfo(RadioMetadata.newLAV("offensive 音mad", "KrazedDonut", RadioMetadata.createYouTubeURI("glDecH2ALUo")).bOfficial()) + .addItemInfo(RadioMetadata.newLAV("With Apologies to Jesse Jackson (S11E1)", "South Park", RadioMetadata.createYouTubeURI("3YC1_RUuzWg")).setFor(0).bOriginal().bExplicit()) + .addItemInfo(RadioMetadata.newLA("【疾走アレンジ】砕月~ぺったんファイヤー☆", "さわわ@疾走の人", RadioMetadata.createNicoNicoURI("sm19473562")).setFor(1).bOriginal().bOfficial()) + .addSource(RadioSource.new("video/mp4", [ "avc1.4D4029", "mp4a.40.2" ]) + .setURI("mxc://glowers.club/kZgNCAbaJycuebFjrGjWRTEX") + .addPreview("mxc://glowers.club/SBNLmftPiqAThhqYEosbQBzH", "image/jpg", "800x450")) + .addSource(RadioSource.new("audio/mp3", [ "mp3" ]) + .setURI("mxc://glowers.club/BifXjxnFEpJqoPBlEIUeJebD") + .addPreview("mxc://glowers.club/ZCOzqccZAuiEaYpUnkWMmDti", "image/jpg", "450x450")), + + RadioItem.new("shutupkyle") + .setMiscInfo(RadioMiscInfo.new().bAltMedia()) + .addItemInfo(RadioMetadata.newLA("Peter Beats Up Kyle Vocoded to Crab Rave", "Amplify", RadioMetadata.createYouTubeURI("6RXNzSbCZ5U")).bOfficial()) + .addItemInfo(RadioMetadata.newLV(null, "Family Guy", RadioMetadata.createYouTubeURI("0TBlyr7gUBc")).setFor(0).bOriginal()) + .addSource(RadioSource.new("video/mp4", [ "avc1.4D4029", "mp4a.40.2" ]) + .setURI("mxc://glowers.club/oJHYSoVkcvwfbRzuEOsjkzfN") + .addPreview("mxc://glowers.club/dlCqoJJRzhlSBBYuYhyCkTKP", "image/jpg", "480x360")) + .addSource(RadioSource.new("audio/mp3", [ "mp3" ]) + .setURI("mxc://glowers.club/NTyheUZmvVEEumjfAAWSMNPp") + .addPreview("mxc://glowers.club/JirPlcPtkKUKAgxFfxzUYkhQ", "image/jpg", "259x259")), + + RadioItem.new("emergencypie") + .addItemInfo(RadioMetadata.newLAV("funny pie ytpmv 2", "KrazedDonut", RadioMetadata.createYouTubeURI("IpW1lHLN-SQ")).bOfficial()) + .addItemInfo(RadioMetadata.newLAV("In HoNoR oF oUr MoSt FuNnY tIkToK", "jaydencroes", RadioMetadata.createTikTokURI("6803451075920776454", "jaydencroes")).setFor(0).bOriginal().bOfficial()) + .addItemInfo(RadioMetadata.newLA("Stage 4-2 (Emergency Avoidance)", "増子司", RadioMetadata.createYouTubeURI("CFdW4abh69U")).setFor(1).bOriginal()) + .addTags([ "ylyl" ]) + .addSource(RadioSource.new("video/mp4", [ "avc1.4D4029", "mp4a.40.2" ]) + .setURI("mxc://glowers.club/VFwITHQRDUeHeqWShuSQvVOR") + .addPreview("mxc://glowers.club/OwnYCvMWJOZVmNbBZRPjcRtY", "image/jpg", "800x450")) + .addSource(RadioSource.new("audio/mp3", [ "mp3" ]) + .setURI("mxc://glowers.club/OAchunTPIXjGSJMeAwJUYJlx") + .addPreview("mxc://glowers.club/VlLkWDBhkHwtdzyihjssmHoL", "image/jpg", "350x350")), + + RadioItem.new("sexed") + .addItemInfo(RadioMetadata.newLAV("sex ed ytpmv", "KrazedDonut", RadioMetadata.createYouTubeURI("XM4VcOKvg1U")).bOfficial()) + .addItemInfo(RadioMetadata.newLAV("No Laughing Part 2 (S2E19)", "Beavis and Butt-Head", RadioMetadata.createYouTubeURI("7hfKrBd34bE")).setFor(0).bOriginal()) + .addItemInfo(RadioMetadata.newLA("Treasure Trove Cove", "Grant Kirkhope", RadioMetadata.createYouTubeURI("5DQfnj33-QU")).setFor(1).bOriginal()) + .addSource(RadioSource.new("video/mp4", [ "avc1.4D4029", "mp4a.40.2" ]) + .setURI("mxc://glowers.club/RteFUtxPuMscWlROFgwTuHSM") + .addPreview("mxc://glowers.club/HXFwEJIBzhvTscjXjeqKOHBR", "image/jpg", "793x600")) + .addSource(RadioSource.new("audio/mp3", [ "mp3" ]) + .setURI("mxc://glowers.club/XdiNyKBZgMFnhTsYBscroXWQ") + .addPreview("mxc://glowers.club/RPkayZSiHWFIFgslxZDtACij", "image/jpg", "500x500")), + + RadioItem.new("eternalsummer") + .addItemInfo(RadioMetadata.newLAV("Goodbye Vine, Hello TikTok", "NoahThePro", RadioMetadata.createYouTubeURI("lxIp370KaWw")).bOfficial()) + .addItemInfo(RadioMetadata.newLAV("In HoNoR oF oUr MoSt FuNnY tIkToK", "jaydencroes", RadioMetadata.createTikTokURI("6803451075920776454", "jaydencroes")).setFor(0).bOriginal().bOfficial()) + .addItemInfo(RadioMetadata.newLA("Goodbye Summer, Hello Winter", "FantoMenK", RadioMetadata.createYouTubeURI("6pKDsdzODv0")).setFor(1).bOriginal().bOfficial()) + .addItemInfo(RadioMetadata.newLA(null, null, RadioMetadata.createAppleMusicURI(426984905)).setFor(1).bOriginal().bOfficial().bAlt()) + .addTags([ "ylyl" ]) + .addSource(RadioSource.new("video/mp4", [ "avc1.4D4029", "mp4a.40.2" ]) + .setURI("mxc://glowers.club/BbQvjOiIVsLGmuhPadpWJccr") + .addPreview("mxc://glowers.club/MtgDrXUhQNnoZFRlHbfJrtCO", "image/jpg", "800x450")) + .addSource(RadioSource.new("audio/mp3", [ "mp3" ]) + .setURI("mxc://glowers.club/ErRgdlBDaOLxSBblNCoGNboe") + .addPreview("mxc://glowers.club/QvSUtxdTfIgogJJNfNrSTgVw", "image/jpg", "500x500")), + + RadioItem.new("stereopie") + .addItemInfo(RadioMetadata.newLAV("TikToks that I committed tax fraud with (TRY NOT TO LAUGH!)", "LNG", RadioMetadata.createYouTubeURI("kpPzCEpN3No")).bOfficial()) + .addItemInfo(RadioMetadata.newLAV("In HoNoR oF oUr MoSt FuNnY tIkToK", "jaydencroes", RadioMetadata.createTikTokURI("6803451075920776454", "jaydencroes")).setFor(0).bOriginal().bOfficial()) + .addItemInfo(RadioMetadata.newLA("Stereo Love", "Edward Maya", RadioMetadata.createYouTubeURI("p-Z3YrHJ1sU")).setFor(1).bOriginal().bOfficial()) + .addItemInfo(RadioMetadata.newLA(null, null, RadioMetadata.createAppleMusicURI(351996354)).setFor(1).bOriginal().bOfficial().bAlt()) + .addTags([ "ylyl" ]) + .addSource(RadioSource.new("video/mp4", [ "avc1.4D4029", "mp4a.40.2" ]) + .setURI("mxc://glowers.club/HiBIQpUfmijfOylsPdSrkdlj") + .addPreview("mxc://glowers.club/RHReavQybpDlkAhZBEYqZPkF", "image/jpg", "800x450")) + .addSource(RadioSource.new("audio/mp3", [ "mp3" ]) + .setURI("mxc://glowers.club/DGCmxthQsYFXdDfcXMbwwlkf") + .addPreview("mxc://glowers.club/KlymZPPhXopZXmmoFrnBUxHp", "image/jpg", "500x500")), + + RadioItem.new("echtermination") + .addItemInfo(RadioMetadata.newLAV("Jontober 19th: echTERMINATION", "KrazedDonut", RadioMetadata.createYouTubeURI("sgdJLXUeaIM")).bOfficial()) + .addItemInfo(RadioMetadata.newLAV("Area 26A (Extermination)", "安井洋介", RadioMetadata.createYouTubeURI("w_j2u9A_1P0")).bOriginal()) + .addSource(RadioSource.new("video/mp4", [ "avc1.4D4029", "mp4a.40.2" ]) + .setURI("mxc://glowers.club/ZQqbMlxGMElwFcspNRQEMPlk") + .addPreview("mxc://glowers.club/hugzqYBVrLCiuuzRsuJUbfMn", "image/jpg", "800x450")) + .addSource(RadioSource.new("audio/mp3", [ "mp3" ]) + .setURI("mxc://glowers.club/qdQhnOJlyrwVrACNaKFQmBAb") + .addPreview("mxc://glowers.club/UwhhKcIhyuKTUXGqECMvpUpF", "image/jpg", "500x500")), + + RadioItem.new("jonumiru") + .addItemInfo(RadioMetadata.newLAV("Jontober 30th - JONUMIRU", "JoeCrapsArt", RadioMetadata.createYouTubeURI("o_cdvj8z8AY")).bOfficial()) + .addItemInfo(RadioMetadata.newLAV("チルミルチルノ", "Conagusuri", RadioMetadata.createInternetArchiveURI("chirumiru-cirno")).bOriginal()) + .addSource(RadioSource.new("video/mp4", [ "avc1.4D4029", "mp4a.40.2" ]) + .setURI("mxc://glowers.club/LQzEXCCYoQrpZxnlHliWAyJG") + .addPreview("mxc://glowers.club/rFxzkmcazQFzIuTiwZOWlVOP", "image/jpg", "800x600")) + .addSource(RadioSource.new("audio/mp3", [ "mp3" ]) + .setURI("mxc://glowers.club/drGNuFgYuOLGFOcTdGbplHjg") + .addPreview("mxc://glowers.club/WAMOSUPXcaBvgAmlpWbBQleO", "image/jpg", "500x500")), + + RadioItem.new("rickbuster") + .addItemInfo(RadioMetadata.newLAV("rick buster", "KrazedDonut", RadioMetadata.createYouTubeURI("JsZw5aNZoas")).bOfficial()) + .addItemInfo(RadioMetadata.newLAV("Never Gonna Give You Up", "Rick Astley", RadioMetadata.createYouTubeURI("dQw4w9WgXcQ")).setFor(0).bOriginal().bOfficial()) + .addItemInfo(RadioMetadata.newLA(null, null, RadioMetadata.createAppleMusicURI(1558534271)).setFor(0).bOriginal().bOfficial().bAlt()) + .addItemInfo(RadioMetadata.newLA("Rude Buster", "Toby Fox", RadioMetadata.createYouTubeURI("GPL5Hkl11IQ")).setFor(1).bOriginal().bOfficial()) + .addItemInfo(RadioMetadata.newLA(null, null, RadioMetadata.createAppleMusicURI(1443475721)).setFor(1).bOriginal().bOfficial().bAlt()) + .addSource(RadioSource.new("video/mp4", [ "avc1.4D4029", "mp4a.40.2" ]) + .setURI("mxc://glowers.club/QvTUdwqkAhCkyUKmawmeyulU") + .addPreview("mxc://glowers.club/WQnVFfEGtkPPZfHQnceGewvH", "image/jpg", "800x600")) + .addSource(RadioSource.new("audio/mp3", [ "mp3" ]) + .setURI("mxc://glowers.club/FgYLPZllWHGsjXOafXTKZEWl") + .addPreview("mxc://glowers.club/pzlFVETfNbFvCvpoafFZJZPv", "image/jpg", "250x250")), + + RadioItem.new("21stcenturyhumor") + .addItemInfo(RadioMetadata.newLAV("21st century humor music video", "mal", RadioMetadata.createYouTubeURI("zZsmA9jObbk")).bOfficial()) + .addItemInfo(RadioMetadata.newLAV("The Next Episode (feat. Snoop Dogg)", "Dr. Dre", RadioMetadata.createYouTubeURI("QZXc39hT8t4")).setFor(0).bOriginal().bOfficial()) + .addItemInfo(RadioMetadata.newLA(null, null, RadioMetadata.createAppleMusicURI(1440783384)).setFor(0).bOriginal().bOfficial().bAlt()) + .addSource(RadioSource.new("video/mp4", [ "avc1.4D4029", "mp4a.40.2" ]) + .setURI("mxc://glowers.club/QtGmvTgoSXrnRCuBmPdMuLYZ") + .addPreview("mxc://glowers.club/VPnfoDHobmgEdsJVFEYcPgVR", "image/jpg", "360x360")) + .addSource(RadioSource.new("audio/mp3", [ "mp3" ]) + .setURI("mxc://glowers.club/iczVPelXBMDHfVRohfWLtZBu") + .addPreview("mxc://glowers.club/QrJEgjpKMgxnIRCzNEOOFjVj", "image/jpg", "360x360")), + + RadioItem.new("piejection") + .addItemInfo(RadioMetadata.newLAV("cornered pie", "Manlego26", RadioMetadata.createYouTubeURI("0ZgQVpn_VYc")).bOfficial()) + .addItemInfo(RadioMetadata.newLAV("In HoNoR oF oUr MoSt FuNnY tIkToK", "jaydencroes", RadioMetadata.createTikTokURI("6803451075920776454", "jaydencroes")).setFor(0).bOriginal().bOfficial()) + .addItemInfo(RadioMetadata.newLAV("Pursuit ~ Cornered", "杉森雅和", RadioMetadata.createYouTubeURI("UxnvGDK0WGM")).setFor(1).bOriginal()) + .addTags([ "ylyl" ]) + .addSource(RadioSource.new("video/mp4", [ "avc1.4D4029", "mp4a.40.2" ]) + .setURI("mxc://glowers.club/MYzLmUwoXexAkKmzXFnRnKxS") + .addPreview("mxc://glowers.club/oYXifvlcLCOLJESENpzUTMCm", "image/jpg", "800x450")) + .addSource(RadioSource.new("audio/mp3", [ "mp3" ]) + .setURI("mxc://glowers.club/nMeEwePIzXGmGExipZUTSQqz") + .addPreview("mxc://glowers.club/TWDjcpCgIdZIykkHtTmMPJNS", "image/jpg", "390x390")), + + RadioItem.new("badapple") + .setMiscInfo(RadioMiscInfo.new().bMetadataPreferId()) + .addItemInfo(RadioMetadata.newLAV("Bad Apple!! but its Vintage Mac sounds", "Raw Elements", RadioMetadata.createYouTubeURI("I7B_OQ1qn44")).bOfficial()) + .addItemInfo(RadioMetadata.newLA("Bad Apple!!", "nomico", RadioMetadata.createNicoNicoURI("sm8628149")).setFor(0).bOriginal()) + .addItemInfo(RadioMetadata.newLA(null, null, RadioMetadata.createAppleMusicURI(1440636627)).setFor(0).bOriginal().bOfficial().bAlt()) + .addSource(RadioSource.new("video/mp4", [ "avc1.4D4029", "mp4a.40.2" ]) + .setURI("mxc://glowers.club/vTjtZkwykdUKcTDGVziJwXHv") + .addPreview("mxc://glowers.club/OgAhAkVDicCifAJmdrhePLrT", "image/jpg", "800x450")) + .addSource(RadioSource.new("audio/mp3", [ "mp3" ]) + .setURI("mxc://glowers.club/ZdoDgZQdPUPEaJqgfqXtAPAn") + .addPreview("mxc://glowers.club/gEDWLnUzgvGmDWwIAuIMDmcX", "image/jpg", "500x500")), + + 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")), + + RadioItem.new("bonuspie") + .setMiscInfo(RadioMiscInfo.new().bMetadataPreferId()) + .addItemInfo(RadioMetadata.newLAV("i baked you a bonus room", "seki", RadioMetadata.createYouTubeURI("8yoda3JQfVE")).bOfficial()) + .addItemInfo(RadioMetadata.newLA("Bonus Room Blitz", "Jake Kaufman", RadioMetadata.createYouTubeURI("zXJtgIwPLTA")).setFor(0).bOriginal()) + .addTags([ "ylyl" ]) + .addSource(RadioSource.new("video/mp4", [ "avc1.4D4029", "mp4a.40.2" ]) + .setURI("mxc://glowers.club/RettRchnnJFSpMrffDAlAyhm") + .addPreview("mxc://glowers.club/eAYaMQSCbyLAGxhAvcoPxCfO", "image/jpg", "800x450")) + .addSource(RadioSource.new("audio/mp3", [ "mp3" ]) + .setURI("mxc://glowers.club/UphXdyanOsNnuwyBMomhsKYF") + .addPreview("mxc://glowers.club/ZNZnUEjHrBxDZAINwaXSbxMw", "image/jpg", "430x430")), + + RadioItem.new("preciouswindows") + .addItemInfo(RadioMetadata.newLAV("Windowsが大切なファイルを盗んだ", "fennecHex", RadioMetadata.createYouTubeURI("UptbjNjPjE0")).bOfficial()) + .addItemInfo(RadioMetadata.newLAV("魔理沙は大変なものを盗んでいきました", "IOSIS", RadioMetadata.createInternetArchiveURI("precious_thing")).setFor(0).bOriginal()) + .addSource(RadioSource.new("video/mp4", [ "avc1.4D4029", "mp4a.40.2" ]) + .setURI("mxc://glowers.club/ZVEWHCaQvDwUTMNFmMYaoTsl") + .addPreview("mxc://glowers.club/oxMUApdwmYtgIDkXacPVVOIP", "image/jpg", "800x450")) + .addSource(RadioSource.new("audio/mp3", [ "mp3" ]) + .setURI("mxc://glowers.club/WDAmEuAHkKqndZvANrwJQSzB") + .addPreview("mxc://glowers.club/nqjlJDKzeJHdonTlxaoRPXsk", "image/jpg", "493x493")), + + RadioItem.new("quagmire") + .setMiscInfo(RadioMiscInfo.new().bMetadataPreferId()) + .addItemInfo(RadioMetadata.newLAV("ytpmv quagmire", "ManGuyAlt", RadioMetadata.createYouTubeURI("8yoda3JQfVE")).bOfficial()) + .addItemInfo(RadioMetadata.newLA("The World Revolving", "Toby Fox", RadioMetadata.createYouTubeURI("Z01Tsgwe2dQ")).setFor(0).bOriginal().bOfficial()) + .addItemInfo(RadioMetadata.newLA(null, null, RadioMetadata.createAppleMusicURI(1443475747)).setFor(0).bOriginal().bOfficial().bAlt()) + .addSource(RadioSource.new("video/mp4", [ "avc1.4D4029", "mp4a.40.2" ]) + .setURI("mxc://glowers.club/gXaCMGISyZNKPyFmYmKutopp") + .addPreview("mxc://glowers.club/hOVpKbqGxKKvzpujMiRhcnzg", "image/jpg", "800x450")) + .addSource(RadioSource.new("audio/mp3", [ "mp3" ]) + .setURI("mxc://glowers.club/EZBKbhtPQBvrHACxCtiHmrSs") + .addPreview("mxc://glowers.club/kWIPOTRFxeRmQNbGZWYQBorZ", "image/jpg", "64x64")), + + 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")), + + RadioItem.new("tour") + .addItemInfo(RadioMetadata.newLAV("Macky Gee - Tour N Remix", "jaycrusader", RadioMetadata.createSoundcloudURI("macky-gee-tour-n-remix", "user-118096317")).bOfficial()) + .addItemInfo(RadioMetadata.newLA("Tour", "Macky Gee", RadioMetadata.createYouTubeURI("9X4iXYqxlPA")).setFor(0).bOriginal().bOfficial()) + .addItemInfo(RadioMetadata.newLA(null, null, RadioMetadata.createAppleMusicURI(1282939530)).setFor(0).bOriginal().bOfficial().bAlt()) + .addSource(RadioSource.new("audio/mp3", [ "mp3" ]) + .setURI("mxc://glowers.club/WSJRsjEvcJraVfPDiqkrLpjm") + .addPreview("mxc://glowers.club/iKjuRXQirdBJZXJOoNIeBVtm", "video/mp4", [ "avc1.4D4029" ], "500x500", EDisplayType.SQUARE) + .addPreview("mxc://glowers.club/wEaDeptGTTtlhzdyVsogJjHa", "image/gif", "250x250", EDisplayType.SQUARE)), + + RadioItem.new("oppanmachine") + .addItemInfo(RadioMetadata.newLAV("Porter Robinson - Oppan Sad Machine", "응과사전, Shittypedia - Free Shit Encyclopedia", RadioMetadata.createSoundcloudURI("porter-robinson-oppan-sad-machine", "shittypedia")).bOfficial()) + .addItemInfo(RadioMetadata.newLA("Sad Machine", "Porter Robinson", RadioMetadata.createYouTubeURI("HAIDqt2aUek")).setFor(0).bOriginal().bOfficial()) + .addItemInfo(RadioMetadata.newLA(null, null, RadioMetadata.createAppleMusicURI(1440858256)).setFor(0).bOriginal().bOfficial().bAlt()) + .addItemInfo(RadioMetadata.newLA(null, null, RadioMetadata.createSoundcloudURI("sad-machine", "porter-robinson")).setFor(0).bOriginal().bOfficial().bAlt().bPremium()) + .addItemInfo(RadioMetadata.newLA("Gangnam Style", "PSY", RadioMetadata.createYouTubeURI("9bZkp7q19f0")).setFor(1).bOriginal().bOfficial()) + .addItemInfo(RadioMetadata.newLA(null, null, RadioMetadata.createAppleMusicURI(1445144527)).setFor(1).bOriginal().bOfficial().bAlt()) + .addSource(RadioSource.new("audio/mp3", [ "mp3" ]) + .setURI("mxc://glowers.club/kXlzwGxKEisLOloneSSSDcjr") + .addPreview("mxc://glowers.club/QlWvAoOFKvkoSpMTfJkGeozg", "image/jpg", "500x500")), + +] diff --git a/lib/RadioItem.js b/lib/RadioItem.js new file mode 100644 index 0000000..ed63eed --- /dev/null +++ b/lib/RadioItem.js @@ -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 } diff --git a/lib/RadioMetadata.js b/lib/RadioMetadata.js new file mode 100644 index 0000000..364f380 --- /dev/null +++ b/lib/RadioMetadata.js @@ -0,0 +1,192 @@ +"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 {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 } diff --git a/lib/RadioMiscInfo.js b/lib/RadioMiscInfo.js new file mode 100644 index 0000000..e020190 --- /dev/null +++ b/lib/RadioMiscInfo.js @@ -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 } diff --git a/lib/RadioPlaybackInfo.js b/lib/RadioPlaybackInfo.js new file mode 100644 index 0000000..7afa09c --- /dev/null +++ b/lib/RadioPlaybackInfo.js @@ -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 } diff --git a/lib/RadioPreview.js b/lib/RadioPreview.js new file mode 100644 index 0000000..f7b2a24 --- /dev/null +++ b/lib/RadioPreview.js @@ -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 } diff --git a/lib/RadioSource.js b/lib/RadioSource.js new file mode 100644 index 0000000..7a396a4 --- /dev/null +++ b/lib/RadioSource.js @@ -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 } diff --git a/lib/build.js b/lib/build.js new file mode 100644 index 0000000..ca8270c --- /dev/null +++ b/lib/build.js @@ -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", ``) diff --git a/lib/lint.js b/lib/lint.js new file mode 100644 index 0000000..3fc65be --- /dev/null +++ b/lib/lint.js @@ -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 } diff --git a/lib/util.js b/lib/util.js new file mode 100644 index 0000000..e49c708 --- /dev/null +++ b/lib/util.js @@ -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 = 7 +const toRadioSongs = songs => ({ + "version": SCHEMA_VERSION, + songs +}) + +module.exports = { + SCHEMA_VERSION, + objSerialize, + objBulkSet, + arrSerializeSort, + fromConstToPascal, + notNullElse, + toRadioSongs +}