[solved] Internet radio (or IPTV) recording script

I was bored and...

These are two scripts: one a recorder and the other a manager. They record online radio. In the first script, you need to paste a link to your radio stream. In the second script manager, in the block responsible for the current song title, you need to:

  1. paste a link to the endpoint from which you can get the song title.
  2. parse the song title from the response, either manually or with AI. There's an example with subtitles on YouTube.

Oh, and you'll also need a FAT32 flash drive.

The script manager needs to be run in the scheduler, every 5-60 seconds, depending on how precise you want the song cuts to be.

Recorder Script

:global bassDriveStartFile
:if ([:len [/file find name=$bassDriveStartFile]] > 0) do={
/file remove [find name=$bassDriveStartFile]
}
/tool fetch url="https://chi2.bassdrive.net/stream" dst-path=$bassDriveStartFile

Manager Script

:global bassDriveCurrentShow
:global bassDriveStartFile
:local scriptRecorder "bassDriveRecorder"
:local diskSlot "usb1"
:local folder ($diskSlot . "/bassDrive/")

:local freeSpace (([/disk print as-value where slot=$diskSlot]->0)->"free")
:if ([:len "$freeSpace"] = 0) do={
    :log warning ("disk unavailable: " . $diskSlot . " script " . $scriptRecorder . " stopped")
    /system script job remove [find script=$scriptRecorder]
    :quit
}

:if ($freeSpace < 104857600) do={
    :log warning ("low disk space on " . $diskSlot . "script " . $scriptRecorder . " stopped")
    /system script job remove [find script=$scriptRecorder]
    :quit
}

# start parse song title block

:local rawTitle
:do {
    :set rawTitle [/tool fetch url="https://bassdrive.com/now-playing.php" output=user as-value]
} on-error={
    :log warning ("get now-playing show failed " . "script " . $scriptRecorder . " stopped") 
    /system script job remove [find script=$scriptRecorder]
    :quit
}

:local html ($rawTitle->"data")
:local spanPos [:find $html "<span"]
:local newShow "Unknown BassDrive Show"

:if ([:len "$spanPos"] > 0) do={
    :set newShow [:pick $html 0 $spanPos]
}

# end parse song title block

:local sanitizeShow
:set sanitizeShow do={
    :local input $1
    :local allowed "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_- "
    :local title ""
    :for i from=0 to=([:len $input] - 1) do={
        :local c [:pick $input $i ($i + 1)]
        :if ($allowed ~ $c) do={
            :set title ($title . $c)
        }
    }

    :while (([:len $title] > 0) && ([:pick $title ([:len $title] - 1)] = " ")) do={
        :set title [:pick $title 0 ([:len $title] - 1)]
    }
    :return $title
}

:local cleanName [$sanitizeShow $newShow]
#:log info ("bassDrive current show: " . $newShow . "clean: " . $cleanName)
:local date ([/system clock get date] . "/")
:local newFileName ($folder . $date . $cleanName . ".mp3")
:local finalName $newFileName
:local counter 1
:while ([:len [/file find name=$finalName]] > 0) do={
    :set finalName ($folder . $date . $cleanName . "_" . $counter . ".mp3")
    :set counter ($counter + 1)
    }

:if ([:len [/system script job find script=$scriptRecorder]] = 0) do={
    :set bassDriveStartFile ($folder . "bassDriveLive.mp3")
    :set bassDriveCurrentShow $newShow
    /system script run $scriptRecorder

} else={
   :if ($bassDriveCurrentShow != $newShow) do={
        /system script job remove [find script=$scriptRecorder]
        :log info "bassDrive new show started"
        :set bassDriveCurrentShow $newShow
        /system script run $scriptRecorder
    } else={
       :if ([:len [/file find name=$bassDriveStartFile]] > 0) do={
        /file set name=$finalName $bassDriveStartFile     
       }
    }
}

The scheduler starts the manager, and the manager starts the recorder. To stop recording, stop the scheduler and delete running scripts in Scripts/Jobs.

The result will be something like this

UPD:
Quite by accident, I had to create a script to record a CCTV camera from a free cloud service. Which naturally ended up being a script for IPTV recording.

For example, this channel

#EXTINF:-1 tvg-id="",Gaki no Tsukai (English Subs) (720p)
https://hamada.gaki-no-tsukai.stream/hls/test.m3u8

Answering like this

#EXTM3U
#EXT-X-VERSION:3
#EXT-X-MEDIA-SEQUENCE:2479
#EXT-X-TARGETDURATION:11
#EXTINF:3.567,
test-2479.ts
#EXTINF:6.033,
test-2480.ts
#EXTINF:8.334,
test-2481.ts
#EXTINF:3.333,
test-2482.ts

You can download using a script. iptvRecorder

:local iptvBaseUrl "https://hamada.gaki-no-tsukai.stream/hls/"
:local iptvListUrl ($iptvBaseUrl . "test.m3u8")
:local diskSlot "usb1"

:local freeSpace (([/disk print as-value where slot=$diskSlot]->0)->"free")
# check disk (100 MB)
:if (([:len "$freeSpace"] = 0) || ($freeSpace < 104857600)) do={
    :log warning ("low disk space on " . $diskSlot)
    :quit
}

:local getFolderPath do={
    :local sDate [/system clock get date]
    :local sTime [/system clock get time]
    :return ("usb1/iptv_channel/" . [:pick $sDate 0 4] . [:pick $sDate 5 7] . [:pick $sDate 8 10] . "/" . [:pick $sTime 0 2])
}
:local videoDateFolder [$getFolderPath]
:do {
    :local playlistContent
    :local startPosTsLink
    :local endPosTsLink
    :local hasChunks true
    :local hasChunk true
    :local playlistDelay 1
    :while ($hasChunks) do={
        :delay $playlistDelay
        :set playlistContent ([/tool fetch url=$iptvListUrl output=user as-value]->"data")
        #:log warning ("playlistContent : " . $playlistContent)
        :local searchPosPointer 0
        :local startPosTsLink [:find $playlistContent ("test-") $searchPosPointer]
        :if ([:len $startPosTsLink] = 0) do={
            :log info ("No Links in List")
            :set hasChunks false
            :set hasChunk false
        } else={
            :set hasChunk true
        }
        :local downloadedCount 0
        :while ($hasChunk) do={
            :local endPosTsLink [:find $playlistContent "\n" $startPosTsLink]
            :local segmentFile [:pick $playlistContent $startPosTsLink $endPosTsLink]
            #:log info ("Found chunk: " . $segmentFile)
            :set videoDateFolder [$getFolderPath]
            :local chunkFileLocalPath ($videoDateFolder . "/" . $segmentFile)
            :if ([:len [/file find where name=$chunkFileLocalPath]] = 0) do={
                :set freeSpace (([/disk print as-value where slot=$diskSlot]->0)->"free")
                :if (([:len "$freeSpace"] = 0) || ($freeSpace < 104857600)) do={
                    :log warning ("low disk space on " . $diskSlot)
                    :quit
                }
                :local chunkFileUrl ($iptvBaseUrl . $segmentFile)
                :log warning ("Downloading: " . $chunkFileUrl)
                :do {
                    :delay 200ms
                    /tool fetch url=$chunkFileUrl dst-path=$chunkFileLocalPath keep-result=yes
                    :set downloadedCount ($downloadedCount + 1)
                } on-error={
                    :log warning ("Failed to download chunk: " . $chunkFileUrl)
                }
            }
            :set searchPosPointer ($endPosTsLink + 1)
            :set startPosTsLink [:find $playlistContent ("test-") $searchPosPointer]
            :if ([:len $startPosTsLink] = 0) do={
                :log info ("End Links")
                :set hasChunk false
            }
        }
        :if ($hasChunks) do={
            :if ($downloadedCount > 0) do={
                :local calculatedDelay (10 - (2 * $downloadedCount))
                :if ($calculatedDelay < 2) do={
                    :set calculatedDelay 200ms
                }
                :set playlistDelay $calculatedDelay
                :log info "HLS_Parser: Downloaded $downloadedCount chunks. Next playlist check in $playlistDelay s."
            } else={
                :set playlistDelay 10
                :log info "HLS_Parser: No NEW chunks in playlist. Cooling down for 10s."
            }
        }
    }
    :log info "HLS_Parser: All segments from current playlist verified."
} on-error={
    :log error ("HLS_Parser ERROR: ")
}

Running it every 15 seconds

:if ([:len [/system script job find script=iptvRecorder]] = 0) do={
/system script run iptvRecorder
} else={
:log warning "iptvRecorder still running"
}

All downloaded .ts parts can be merged into an mp4 file using ffmpeg.exe with two commands.

copy /b *.ts merged.ts
ffmpeg -i merged.ts -c copy ready_video.mp4

Cool script, very unique use case! Also great song choices :slight_smile: always good to see fellow DnB/Jungle listeners, especially of the older kind like yours!

Thanks :headphone:

Not only is this wildly random, but more, I can totally see Mikrotik would take this seriously and release jukebox router or smth.

Anyway, well done Sir.

The big bgp-mpls guys won't let it happen. :smiley:

Idk, considering some of their recent products, they just might.

Also, technically speaking, their hap be3 has Matter support, which technically makes it (among others) a smart home hub, which is one step away from a smart speaker. So, not only is it possible, we’re half-way there already.

They already support DNLA...