2023-01-01 06:42:34 +01:00
require " json "
2023-01-02 07:51:41 +01:00
require " db "
2023-01-01 06:42:34 +01:00
2022-12-31 14:25:43 +01:00
include CrystalGauntlet
module CrystalGauntlet::Songs
extend self
2023-01-01 06:42:34 +01:00
GD_AUDIO_FORMAT = " mp3 "
2023-01-02 07:51:41 +01:00
# todo: make this configurable
REUPLOADED_SONG_ADD_ID = 5000000
CUSTOM_SONG_START = 50
# set in 6_songs.sql
UNKNOWN_SONG_AUTHOR = 1
2022-12-31 14:25:43 +01:00
def is_custom_song ( id )
2023-01-02 07:51:41 +01:00
id >= CUSTOM_SONG_START
2022-12-31 14:25:43 +01:00
end
def is_reuploaded_song ( id )
2023-01-02 07:51:41 +01:00
id >= REUPLOADED_SONG_ADD_ID
2022-12-31 14:25:43 +01:00
end
2023-01-01 06:42:34 +01:00
2023-01-02 07:51:41 +01:00
class SongMetadata
def initialize ( name : String , author : String , normalized_url : String , source : String , author_url : String , duration : Int32 | Nil )
2023-01-01 06:42:34 +01:00
@name = name
@author = author
2023-01-02 07:51:41 +01:00
@normalized_url = normalized_url
@source = source
@author_url = author_url
@duration = duration
end
def name
@name
end
def author
@author
end
def normalized_url
@normalized_url
end
def source
@source
end
def author_url
@author_url
end
def duration
@duration
2023-01-01 06:42:34 +01:00
end
end
def is_source_allowed ( source : String ) : Bool
config_get ( " songs.allow_all_sources " ) . as? ( Bool ) || config_get ( " songs.sources. #{ source } .allow " ) . as? ( Bool ) || false
end
2023-01-02 07:51:41 +01:00
def get_file_path ( song_id : Int32 )
Path . new ( " data " , " #{ song_id } .mp3 " )
end
# will raise errors
def fetch_song_metadata ( url : String ) : SongMetadata
puts " getting metadata for #{ url } "
2023-01-01 06:42:34 +01:00
output = IO :: Memory . new
# todo: ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️ LOOK OUT FOR SHELL INJECTION BULLSHIT!!!!!!!!!!!!!!!!!!
Process . run ( config_get ( " songs.sources.ytdlp_binary " ) . as? ( String ) || " yt-dlp " , [ " -J " , url ] , output : output )
output . close
metadata = JSON . parse ( output . to_s )
2023-01-02 07:51:41 +01:00
canonical_url = metadata [ " webpage_url " ] . as_s? || metadata [ " original_url " ] . as_s? || url
duration = metadata [ " duration " ]? && ( metadata [ " duration " ] . as_f? || metadata [ " duration " ] . as_i? )
return SongMetadata . new (
( metadata [ " fulltitle " ]? && metadata [ " fulltitle " ] . as_s? ) || ( metadata [ " title " ]? && metadata [ " title " ] . as_s? ) || url ,
( metadata [ " uploader " ]? && metadata [ " uploader " ] . as_s? ) || " " ,
canonical_url ,
metadata [ " extractor " ] . as_s ,
( metadata . [ " uploader_url " ]? && metadata [ " uploader_url " ] . as_s? ) || canonical_url ,
duration ? duration . to_i : nil
)
end
# name, author id, author name, size, download url
# returns nil if song should be disabled
# throws if something failed
def fetch_song ( song_id : Int32 , get_download = false ) : Tuple ( String , Int32 , String , Int32 | Nil , String | Nil ) | Nil
puts " fetching #{ song_id } "
if ! config_get ( " songs.allow_custom_songs " ) . as? ( Bool )
puts " custom songs not allowed "
return nil
end
# todo: this is kinda spaghetti
metadata = nil
author_id = nil
size = nil
fetch_url = nil
song_exists = false
url = nil
begin
url , disabled = DATABASE . query_one ( " select url, disabled from songs where id = ? " , song_id , as : { String , Bool } )
if disabled
return nil
end
song_exists = true
rescue
if config_get ( " songs.preserve_newgrounds_ids " ) . as? ( Bool )
url = " https://www.newgrounds.com/audio/listen/ #{ song_id } "
else
raise " unknown song ID "
end
2023-01-01 06:42:34 +01:00
end
2023-01-02 07:51:41 +01:00
if DATABASE . scalar ( " select count(*) from song_data where id = ? " , song_id ) . as ( Int64 ) > 0
song_name , song_author_id , song_author_name , song_author_url , song_size , song_source , song_duration , download_url = DATABASE . query_one ( " select song_data.name, author_id, song_authors.name, song_authors.url, size, song_data.source, duration, proxy_url from song_data left join song_authors on song_authors.id = song_data.author_id where song_data.id = ? " , song_id , as : { String , Int32 , String ?, String ?, Int32 ?, String , Int32 ?, String ?} )
2023-01-01 06:42:34 +01:00
2023-01-02 07:51:41 +01:00
fetch_url = download_url
size = song_size
author_id = song_author_id
metadata = SongMetadata . new ( song_name , song_author_name || " " , url . not_nil! , song_source , song_author_url || " " , song_duration )
else
begin
metadata = fetch_song_metadata ( url . not_nil! )
rescue err
puts " ran into error fetching metadata: #{ err } ; disabling song "
puts err . inspect
if song_exists
DATABASE . exec ( " update songs set disabled=1 where id = ? " , song_id )
else
DATABASE . exec ( " insert into songs (id, url, disabled) values (?, ?, 1) " , song_id , url )
end
else
if song_exists && url != metadata . normalized_url
DATABASE . exec ( " update songs set url = ? where id = ? " , metadata . normalized_url , song_id )
end
if DATABASE . scalar ( " select count(*) from songs join song_data on songs.id = song_data.id where songs.id != ? and url = ? " , song_id , metadata . normalized_url ) . as ( Int64 ) > 0
# just use that song's metadata instead
# todo: dedup this and the above similar block somehow?
song_name , song_author_id , song_author_name , song_author_url , song_size , song_source , song_duration , download_url = DATABASE . query_all ( " select song_data.name, author_id, song_authors.name, song_authors.url, size, song_data.source, duration, proxy_url from song_data left join songs on song_data.id = songs.id left join song_authors on song_authors.id = song_data.author_id where song_data.id != ? and songs.url = ? " , song_id , metadata . normalized_url , as : { String , Int32 , String ?, String ?, Int32 ?, String , Int32 ?, String ?} ) [ 0 ]
fetch_url = download_url
size = song_size
author_id = song_author_id
metadata = SongMetadata . new ( song_name , song_author_name || " " , url . not_nil! , song_source , song_author_url || " " , song_duration )
end
2023-01-01 06:42:34 +01:00
end
end
2023-01-02 07:51:41 +01:00
puts metadata . inspect
# todo: insert into song_data
# do checks to make sure this is a valid song
max_duration = config_get ( " songs.sources.max_duration " ) . as? ( Int64 )
# todo
if ( fetch_url || ! get_download ) && metadata && size && author_id
# we're done! woo
if fetch_url && fetch_url . starts_with? ( " ./ " )
# todo
fetch_url = " localhost:8080/ #{ fetch_url [ 2 .. ] } "
2023-01-01 06:42:34 +01:00
end
2023-01-02 07:51:41 +01:00
return { metadata . name , author_id , metadata . author , size , fetch_url }
end
2023-01-01 06:42:34 +01:00
2023-01-02 07:51:41 +01:00
metadata = metadata . not_nil!
2023-01-01 06:42:34 +01:00
2023-01-02 07:51:41 +01:00
if get_download
if config_get ( " songs.sources.allow_transcoding " )
if ! config_get ( " songs.sources.proxy_downloads " ) . as? ( Bool )
raise " can't download a song with transcoding but without proxying allowed "
end
2023-01-01 06:42:34 +01:00
2023-01-02 07:51:41 +01:00
# todo: check if song file exists
2023-01-01 06:42:34 +01:00
2023-01-02 07:51:41 +01:00
target_path = get_file_path ( song_id )
2023-01-01 06:42:34 +01:00
2023-01-02 07:51:41 +01:00
Process . run ( config_get ( " songs.sources.ytdlp_binary " ) . as? ( String ) || " yt-dlp " , [ " -f " , " ba " , " -x " , " --audio-format " , GD_AUDIO_FORMAT , " -o " , target_path . to_s , " --ffmpeg-location " , config_get ( " songs.sources.ffmpeg_binary " ) . as? ( String ) || " ffmpeg " , metadata . normalized_url ] , output : STDOUT , error : STDOUT )
size = File . size ( target_path ) . to_i
fetch_url = " ./ #{ song_id } .mp3 "
else
# todo
raise " fetching songs without transcoding and proxying downloads currently unimplemented "
end
# todo: update song_data with size, duration and url
end
if ! author_id
# todo
author_id = 1
end
if fetch_url && fetch_url . starts_with? ( " ./ " )
2023-01-01 06:42:34 +01:00
# todo
2023-01-02 07:51:41 +01:00
# todo also: deduplicate this with similar block above?
fetch_url = " localhost:8080/ #{ fetch_url [ 2 .. ] } "
2023-01-01 06:42:34 +01:00
end
2023-01-02 07:51:41 +01:00
return { metadata . name , author_id , metadata . author , size , fetch_url }
2023-01-01 06:42:34 +01:00
end
2022-12-31 14:25:43 +01:00
end