custom songs more-or-less functional

This commit is contained in:
Jill 2022-12-31 16:25:43 +03:00
parent 4693eb222a
commit 865c21c4ea
10 changed files with 157 additions and 10 deletions

13
db/migrations/6_songs.sql Normal file
View File

@ -0,0 +1,13 @@
-- +migrate up
CREATE TABLE songs (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
author_id INTEGER NOT NULL,
author_name TEXT NOT NULL,
size INTEGER NOT NULL, -- in bytes
download TEXT NOT NULL,
disabled INTEGER NOT NULL DEFAULT 0
);
-- +migrate down
DROP TABLE songs;

View File

@ -1,5 +1,9 @@
version: 2.0
shards:
crystagiri:
git: https://github.com/madeindjs/crystagiri.git
version: 0.3.5
db:
git: https://github.com/crystal-lang/crystal-db.git
version: 0.6.0

View File

@ -20,6 +20,8 @@ dependencies:
toml:
github: crystal-community/toml.cr
branch: master
crystagiri:
github: madeindjs/crystagiri
crystal: 1.6.2

View File

@ -11,6 +11,7 @@ require "./lib/format"
require "./lib/accounts"
require "./lib/gjp"
require "./lib/clean"
require "./lib/songs"
Dotenv.load

View File

@ -69,10 +69,11 @@ CrystalGauntlet.endpoints["/downloadGJLevel22.php"] = ->(body : String): String
9 => difficulty ? difficulty.to_star_difficulty : 0, # 0=N/A 10=EASY 20=NORMAL 30=HARD 40=HARDER 50=INSANE 50=AUTO 50=DEMON
10 => downloads,
11 => 1,
12 => song_id < 50 ? song_id : 0,
12 => !Songs.is_custom_song(song_id) ? song_id : 0,
13 => game_version,
14 => likes,
17 => difficulty && difficulty.demon?,
# 0 for n/a, 10 for easy, 20, for medium, ...
43 => (demon_difficulty || DemonDifficulty::Hard).to_demon_difficulty,
25 => difficulty && difficulty.auto?,
18 => stars || 0,
@ -84,7 +85,7 @@ CrystalGauntlet.endpoints["/downloadGJLevel22.php"] = ->(body : String): String
31 => two_player,
28 => "1",
29 => "1",
35 => song_id >= 50 ? song_id : 0,
35 => Songs.is_custom_song(song_id) ? song_id : 0,
36 => extra_data,
37 => coins,
38 => rated_coins,
@ -93,7 +94,6 @@ CrystalGauntlet.endpoints["/downloadGJLevel22.php"] = ->(body : String): String
47 => 2,
40 => has_ldm,
27 => xor_pass,
# 0 for n/a, 10 for easy, 20, for medium, ...
})
if params.has_key?("extras")

View File

@ -131,7 +131,7 @@ CrystalGauntlet.endpoints["/getGJLevels21.php"] = ->(body : String): String {
# todo: search query
where_str = "where (#{queryParams.join(") and (")})"
query_base = "from levels join users on levels.user_id = users.id #{where_str} order by #{order}"
query_base = "from levels join users on levels.user_id = users.id left join songs on levels.song_id = songs.id #{where_str} order by #{order}"
puts query_base
@ -143,7 +143,7 @@ CrystalGauntlet.endpoints["/getGJLevels21.php"] = ->(body : String): String {
hash_data = [] of Tuple(Int32, Int32, Bool)
DATABASE.query "select levels.id, levels.name, levels.user_id, levels.description, levels.original, levels.game_version, levels.requested_stars, levels.version, levels.song_id, levels.length, levels.objects, levels.coins, levels.has_ldm, levels.two_player, levels.downloads, levels.likes, levels.difficulty, levels.community_difficulty, levels.demon_difficulty, levels.stars, levels.featured, levels.epic, levels.rated_coins, users.username, users.udid, users.account_id, users.registered #{query_base} limit #{levels_per_page} offset #{page_offset}" do |rs|
DATABASE.query "select levels.id, levels.name, levels.user_id, levels.description, levels.original, levels.game_version, levels.requested_stars, levels.version, levels.song_id, levels.length, levels.objects, levels.coins, levels.has_ldm, levels.two_player, levels.downloads, levels.likes, levels.difficulty, levels.community_difficulty, levels.demon_difficulty, levels.stars, levels.featured, levels.epic, levels.rated_coins, users.username, users.udid, users.account_id, users.registered, songs.name, songs.author_id, songs.author_name, songs.size, songs.disabled, songs.download #{query_base} limit #{levels_per_page} offset #{page_offset}" do |rs|
rs.each do
id = rs.read(Int32)
name = rs.read(String)
@ -178,8 +178,15 @@ CrystalGauntlet.endpoints["/getGJLevels21.php"] = ->(body : String): String {
user_account_id = rs.read(Int32 | Nil)
user_registered = rs.read(Bool)
song_name = rs.read(String | Nil)
song_author_id = rs.read(Int32 | Nil)
song_author_name = rs.read(String | Nil)
song_size = rs.read(Int32 | Nil)
song_disabled = rs.read(Int32 | Nil)
song_download = rs.read(String | Nil)
# https://github.com/Cvolton/GMDprivateServer/blob/master/incl/levels/getGJLevels.php#L266
results << CrystalGauntlet::Format.fmt_hash({
results << Format.fmt_hash({
1 => id,
2 => name,
5 => version,
@ -187,10 +194,11 @@ CrystalGauntlet.endpoints["/getGJLevels21.php"] = ->(body : String): String {
8 => 10,
9 => difficulty ? difficulty.to_star_difficulty : 0, # 0=N/A 10=EASY 20=NORMAL 30=HARD 40=HARDER 50=INSANE 50=AUTO 50=DEMON
10 => downloads,
12 => song_id < 50 ? song_id : 0,
12 => !Songs.is_custom_song(song_id) ? song_id : 0,
13 => game_version,
14 => likes,
17 => difficulty && difficulty.demon?,
# 0 for n/a, 10 for easy, 20, for medium, ...
43 => (demon_difficulty || DemonDifficulty::Hard).to_demon_difficulty,
25 => difficulty && difficulty.auto?,
18 => stars || 0,
@ -207,11 +215,25 @@ CrystalGauntlet.endpoints["/getGJLevels21.php"] = ->(body : String): String {
46 => 1,
47 => 2,
40 => has_ldm,
35 => song_id >= 50 ? song_id : 0, # 0 for n/a, 10 for easy, 20, for medium, ...
35 => Songs.is_custom_song(song_id) ? song_id : 0,
})
users << "#{user_id}:#{user_username}:#{user_registered ? user_account_id : user_udid}"
if Songs.is_custom_song(song_id) && song_disabled == 0
songs << Format.fmt_song({
1 => song_id,
2 => song_name,
3 => song_author_id,
4 => song_author_name,
5 => song_size.not_nil! / (1000 * 1000),
6 => "",
10 => song_download,
7 => "",
8 => "1"
})
end
hash_data << {id, stars || 0, rated_coins}
end
end
@ -219,7 +241,7 @@ CrystalGauntlet.endpoints["/getGJLevels21.php"] = ->(body : String): String {
# `${amount}:${offset}:${levelsPerPage}`
searchMeta = "#{level_count}:#{page_offset}:#{levels_per_page}"
res = [results.join("|"), users.join("|"), songs.join("|"), searchMeta, CrystalGauntlet::Hashes.gen_multi(hash_data)].join("#")
res = [results.join("|"), users.join("|"), songs.join("~:~"), searchMeta, CrystalGauntlet::Hashes.gen_multi(hash_data)].join("#")
puts res
res

View File

@ -0,0 +1,87 @@
require "uri"
require "crystagiri"
require "http/client"
require "digest/sha256"
include CrystalGauntlet
NEWGROUNDS_AUDIO_URL_REGEX = /(?<!\\)"url":"(.+?)(?<!\\)"/
def unescape_string(s : String) : String
s.gsub(/\\(.)/) { |v| v[1] }
end
CrystalGauntlet.endpoints["/getGJSongInfo.php"] = ->(body : String): String {
params = URI::Params.parse(body)
puts params.inspect
song_id = params["songID"].to_i32
DATABASE.query("select name, author_id, author_name, size, download, disabled from songs where id = ?", song_id) do |rs|
if rs.move_next
song_name = rs.read(String)
author_id = rs.read(Int32)
author_name = rs.read(String)
size = rs.read(Int32)
download = rs.read(String)
disabled = rs.read(Int32)
if disabled == 1
return "-2"
end
return Format.fmt_song({
1 => song_id,
2 => song_name,
3 => author_id,
4 => author_name,
5 => size / (1000 * 1000),
6 => "",
10 => download,
7 => "",
8 => "0"
})
end
end
if Songs.is_reuploaded_song(song_id)
# todo
"-1"
else
# todo: maybe use yt-dlp? for other sources too
doc = Crystagiri::HTML.from_url "https://www.newgrounds.com/audio/listen/#{song_id}"
song_name = (doc.css("title") { |d| })[0].content
song_artist = (doc.css(".item-details-main > h4 > a") { |d| d })[0].content
song_url_str = (doc.css("script") { |d| })
.map { |d| d.node.to_s.match(NEWGROUNDS_AUDIO_URL_REGEX) }
.reduce { |acc, d| acc || d }
.not_nil![1]
# todo: proxy locally
song_url = unescape_string(song_url_str).split("?")[0].sub("https://", "http://")
# todo: consider hashes?
size = 0
HTTP::Client.head(song_url) do |response|
size = response.headers["content-length"].to_i
end
author_id = 9 # todo: what is this needed for?
DATABASE.exec("insert into songs (id, name, author_id, author_name, size, download) values (?, ?, ?, ?, ?, ?)", song_id, song_name, author_id, song_artist, size, song_url)
return Format.fmt_song({
1 => song_id,
2 => song_name,
3 => author_id,
4 => song_artist,
5 => size / (1000 * 1000),
6 => "",
10 => song_url,
7 => "",
8 => "0"
})
end
}

View File

@ -7,7 +7,7 @@ module CrystalGauntlet::Clean
# these are just the ones commonly used in response formatting
# i'm unsure if any other ones should be added, so for the time
# being i'll just keep it as is
str.gsub(/[:\|~#\(\)\0\n]/, "")
str.gsub(/[:\|~#\(\)\0\n~]/, "")
end
# for descriptions & similar

View File

@ -15,6 +15,10 @@ module CrystalGauntlet::Format
def fmt_hash(hash) : String
hash.map_with_index{ |(i, v)| "#{i}:#{fmt_value(v)}" }.join(":")
end
def fmt_song(hash) : String
hash.map_with_index{ |(i, v)| "#{i}~|~#{fmt_value(v)}" }.join("~|~")
end
end
module CrystalGauntlet::GDBase64

14
src/lib/songs.cr Normal file
View File

@ -0,0 +1,14 @@
include CrystalGauntlet
module CrystalGauntlet::Songs
extend self
def is_custom_song(id)
id >= 50
end
def is_reuploaded_song(id)
# todo: make configurable
id >= 5000000
end
end