diff --git a/code/__HELPERS/~monkestation-helpers/text.dm b/code/__HELPERS/~monkestation-helpers/text.dm
new file mode 100644
index 000000000000..cb6383f2e031
--- /dev/null
+++ b/code/__HELPERS/~monkestation-helpers/text.dm
@@ -0,0 +1,6 @@
+/// Checks to see if a string starts with http:// or https://
+/proc/is_http_protocol(text)
+ var/static/regex/http_regex
+ if(isnull(http_regex))
+ http_regex = new("^https?://")
+ return findtext(text, http_regex)
diff --git a/code/_globalvars/_regexes.dm b/code/_globalvars/_regexes.dm
index 7297d509a918..e850889a42b7 100644
--- a/code/_globalvars/_regexes.dm
+++ b/code/_globalvars/_regexes.dm
@@ -1,7 +1,4 @@
//These are a bunch of regex datums for use /((any|every|no|some|head|foot)where(wolf)?\sand\s)+(\.[\.\s]+\s?where\?)?/i
-GLOBAL_DATUM_INIT(is_http_protocol, /regex, regex("^https?://"))
-GLOBAL_DATUM_INIT(is_http_protocol_non_secure, /regex, regex("^http?://"))
-
GLOBAL_DATUM_INIT(is_website, /regex, regex("http|www.|\[a-z0-9_-]+.(com|org|net|mil|edu)+", "i"))
GLOBAL_DATUM_INIT(is_email, /regex, regex("\[a-z0-9_-]+@\[a-z0-9_-]+.\[a-z0-9_-]+", "i"))
diff --git a/code/modules/admin/verbs/playsound.dm b/code/modules/admin/verbs/playsound.dm
index 784963f00d13..15e12625c8a9 100644
--- a/code/modules/admin/verbs/playsound.dm
+++ b/code/modules/admin/verbs/playsound.dm
@@ -150,7 +150,7 @@
message_admins("[key_name(user)] stopped web sounds.")
web_sound_url = null
stop_web_sounds = TRUE
- if(web_sound_url && !findtext(web_sound_url, GLOB.is_http_protocol))
+ if(web_sound_url && !is_http_protocol(web_sound_url))
tgui_alert(user, "The media provider returned a content URL that isn't using the HTTP or HTTPS protocol. This is a security risk and the sound will not be played.", "Security Risk", list("OK"))
to_chat(user, span_boldwarning("BLOCKED: Content URL not using HTTP(S) Protocol!"), confidential = TRUE)
@@ -183,7 +183,7 @@
if(length(web_sound_input))
web_sound_input = trim(web_sound_input)
- if(findtext(web_sound_input, ":") && !findtext(web_sound_input, GLOB.is_http_protocol))
+ if(findtext(web_sound_input, ":") && !is_http_protocol(web_sound_input))
to_chat(src, span_boldwarning("Non-http(s) URIs are not allowed."), confidential = TRUE)
to_chat(src, span_warning("For youtube-dl shortcuts like ytsearch: please use the appropriate full URL from the website."), confidential = TRUE)
return
diff --git a/code/modules/requests/request_manager.dm b/code/modules/requests/request_manager.dm
index eb16a38fc3bc..408889b86eef 100644
--- a/code/modules/requests/request_manager.dm
+++ b/code/modules/requests/request_manager.dm
@@ -245,7 +245,7 @@ GLOBAL_DATUM_INIT(requests, /datum/request_manager, new)
if(request.req_type != REQUEST_INTERNET_SOUND)
to_chat(usr, "Request doesn't have a sound to play.", confidential = TRUE)
return TRUE
- if(findtext(request.message, ":") && !findtext(request.message, GLOB.is_http_protocol))
+ if(findtext(request.message, ":") && !is_http_protocol(request.message))
to_chat(usr, "Request is not a valid URL.", confidential = TRUE)
return TRUE
diff --git a/monkestation/code/modules/cassettes/cassette_db/cassette_datum.dm b/monkestation/code/modules/cassettes/cassette_db/cassette_datum.dm
index 07efe51ea95e..b86812e3b2e6 100644
--- a/monkestation/code/modules/cassettes/cassette_db/cassette_datum.dm
+++ b/monkestation/code/modules/cassettes/cassette_db/cassette_datum.dm
@@ -1,61 +1,72 @@
-/datum/cassette_data
- var/cassette_name
- var/cassette_author
- var/cassette_desc
- var/cassette_author_ckey
-
- var/cassette_design_front
- var/cassette_design_back
-
- var/list/songs
-
- var/list/song_names
-
- var/cassette_id
- var/approved
- var/file_name
-
-
-/datum/cassette_data/proc/populate_data(file_id)
- var/file = file("data/cassette_storage/[file_id].json")
- if(!fexists(file))
- return FALSE
- var/list/data = json_decode(file2text(file))
-
- cassette_name = data["name"]
- cassette_desc = data["desc"]
-
- cassette_design_front = data["side1_icon"]
- cassette_design_back = data["side2_icon"]
-
- songs = data["songs"]
-
- song_names = data["song_names"]
-
- cassette_author = data["author_name"]
- cassette_author_ckey = data["author_ckey"]
-
- cassette_id = file_id
-
+/datum/cassette
+ /// The name of the cassette.
+ var/name
+ /// The description of the cassette.
+ var/desc
+ /// The unique ID of the cassette.
+ var/id
+ /// If the cassette is approved or not.
+ var/approved = FALSE
+ /// Information about the author of this cassette.
+ var/datum/cassette_author/author
+
+ /// The front side of the cassette.
+ var/datum/cassette_side/front
+ /// The back side of the cassette.
+ var/datum/cassette_side/back
+
+/datum/cassette/New()
+ . = ..()
+ author = new
+ front = new
+ back = new
+
+/// Imports cassette date from the old format.
+/datum/cassette/proc/import_old_format(list/data)
+ name = data["name"]
+ desc = data["desc"]
approved = data["approved"]
- file_name = "data/cassette_storage/[file_id].json"
-
- return TRUE
-
-/datum/cassette_data/proc/generate_cassette(turf/location)
- if(!location)
- return
- var/obj/item/device/cassette_tape/new_tape = new(location)
- new_tape.name = cassette_name
- new_tape.cassette_desc_string = cassette_desc
- new_tape.icon_state = cassette_design_front
- new_tape.side1_icon = cassette_design_front
- new_tape.side2_icon = cassette_design_back
- new_tape.songs = songs
- new_tape.song_names = song_names
- new_tape.author_name = cassette_author
- new_tape.ckey_author = cassette_author_ckey
- new_tape.approved_tape = approved
-
- new_tape.update_appearance()
+ author.name = data["author_name"]
+ author.ckey = ckey(data["author_ckey"])
+
+ for(var/i in 1 to 2)
+ var/datum/cassette_side/side = get_side(i % 2) // side2 = 0, side1 = 1
+ var/side_name = "side[i]"
+ var/list/song_urls = data["songs"][side_name]
+ var/list/song_names = data["song_names"][side_name]
+ if(length(song_urls) != length(song_names))
+ stack_trace("amount of song urls for [side_name] ([length(song_urls)]) did not match amount of song names for [side_name] ([length(song_names)])")
+ continue
+ side.design = data["[side_name]_icon"]
+ for(var/idx in 1 to length(song_urls))
+ side.songs += new /datum/cassette_song(song_names[idx], song_urls[idx])
+
+/// Simple helper to get a side of the cassette.
+/// TRUE is front side, FALSE is back side.
+/datum/cassette/proc/get_side(front_side = TRUE) as /datum/cassette_side
+ RETURN_TYPE(/datum/cassette_side)
+ return front_side ? front : back
+
+/datum/cassette_author
+ /// The character name of the cassette author.
+ var/name
+ /// The ckey of the cassette author.
+ var/ckey
+
+/datum/cassette_side
+ /// The design of this side of the cassette.
+ var/design = "cassette_flip"
+ /// The songs on this side of the cassette.
+ var/list/datum/cassette_song/songs = list()
+
+/datum/cassette_song
+ /// The name of the song.
+ var/name
+ /// The URL of the song.
+ var/url
+
+/datum/cassette_song/New(name, url)
+ . = ..()
+ src.name = name
+ src.url = url
diff --git a/monkestation/code/modules/cassettes/machines/dj_station.dm b/monkestation/code/modules/cassettes/machines/dj_station.dm
index 386de43c5f57..a7742247f810 100644
--- a/monkestation/code/modules/cassettes/machines/dj_station.dm
+++ b/monkestation/code/modules/cassettes/machines/dj_station.dm
@@ -1,6 +1,5 @@
GLOBAL_VAR(dj_broadcast)
-GLOBAL_VAR(dj_booth)
-
+GLOBAL_DATUM(dj_booth, /obj/machinery/cassette/dj_station)
/obj/item/clothing/ears
//can we be used to listen to radio?
@@ -8,42 +7,35 @@ GLOBAL_VAR(dj_booth)
/obj/machinery/cassette/dj_station
name = "Cassette Player"
- desc = "Plays Space Music Board approved cassettes for anyone in the station to listen to "
+ desc = "Plays Space Music Board approved cassettes for anyone in the station to listen to."
icon = 'monkestation/code/modules/cassettes/icons/radio_station.dmi'
icon_state = "cassette_player"
- active_power_usage = BASE_MACHINE_ACTIVE_CONSUMPTION
+ use_power = NO_POWER_USE
+
+ resistance_flags = INDESTRUCTIBLE | LAVA_PROOF | FIRE_PROOF | UNACIDABLE | ACID_PROOF
+ move_resist = MOVE_FORCE_OVERPOWERING
- resistance_flags = INDESTRUCTIBLE
anchored = TRUE
density = TRUE
- var/broadcasting = FALSE
- var/obj/item/device/cassette_tape/inserted_tape
- var/time_left = 0
- var/current_song_duration = 0
- var/list/people_with_signals = list()
- var/list/active_listeners = list()
- var/waiting_for_yield = FALSE
-
- //tape stuff goes here
- var/pl_index = 0
- var/list/current_playlist = list()
- var/list/current_namelist = list()
+ var/broadcasting = FALSE
COOLDOWN_DECLARE(next_song_timer)
/obj/machinery/cassette/dj_station/Initialize(mapload)
. = ..()
REGISTER_REQUIRED_MAP_ITEM(1, INFINITY)
- GLOB.dj_booth = src
register_context()
+ if(QDELETED(GLOB.dj_booth))
+ GLOB.dj_booth = src
/obj/machinery/cassette/dj_station/Destroy()
- . = ..()
- GLOB.dj_booth = null
- STOP_PROCESSING(SSprocessing, src)
+ if(GLOB.dj_booth == src)
+ GLOB.dj_booth = null
+ return ..()
+/*
/obj/machinery/cassette/dj_station/add_context(atom/source, list/context, obj/item/held_item, mob/user)
. = ..()
if(inserted_tape)
@@ -51,327 +43,4 @@ GLOBAL_VAR(dj_booth)
if(!broadcasting)
context[SCREENTIP_CONTEXT_LMB] = "Play Tape"
return CONTEXTUAL_SCREENTIP_SET
-
-/obj/machinery/cassette/dj_station/examine(mob/user)
- . = ..()
- if(time_left > 0 || next_song_timer)
- . += span_notice("It seems to be cooling down, you estimate it will take about [time_left ? DisplayTimeText(((time_left * 10) + 6000)) : DisplayTimeText(COOLDOWN_TIMELEFT(src, next_song_timer))].")
-
-/obj/machinery/cassette/dj_station/process(seconds_per_tick)
- if(waiting_for_yield)
- return
- time_left -= round(seconds_per_tick)
- if(time_left <= 0)
- time_left = 0
- if(COOLDOWN_FINISHED(src, next_song_timer) && broadcasting)
- COOLDOWN_START(src, next_song_timer, 10 MINUTES)
- broadcasting = FALSE
-
-/obj/machinery/cassette/dj_station/attack_hand(mob/user)
- . = ..()
- if(!inserted_tape)
- return
- if((!COOLDOWN_FINISHED(src, next_song_timer)) && !broadcasting)
- to_chat(user, span_notice("The [src] feels hot to the touch and needs time to cooldown."))
- to_chat(user, span_info("You estimate it will take about [time_left ? DisplayTimeText(((time_left * 10) + 6000)) : DisplayTimeText(COOLDOWN_TIMELEFT(src, next_song_timer))] to cool down."))
- return
- message_admins("[src] started broadcasting [inserted_tape] interacted with by [user]")
- logger.Log(LOG_CATEGORY_MUSIC, "[src] started broadcasting [inserted_tape]")
- start_broadcast()
-
-/obj/machinery/cassette/dj_station/AltClick(mob/user)
- . = ..()
- if(!isliving(user) || !user.Adjacent(src))
- return
- if(!inserted_tape)
- return
- if(broadcasting)
- next_song()
-
-/obj/machinery/cassette/dj_station/CtrlClick(mob/user)
- . = ..()
- if(!inserted_tape || broadcasting)
- return
- if(Adjacent(user) && !issiliconoradminghost(user))
- if(!user.put_in_hands(inserted_tape))
- inserted_tape.forceMove(drop_location())
- else
- inserted_tape.forceMove(drop_location())
- inserted_tape = null
- time_left = 0
- current_song_duration = 0
- pl_index = 0
- current_playlist = list()
- current_namelist = list()
- stop_broadcast(TRUE)
-
-/obj/machinery/cassette/dj_station/attackby(obj/item/weapon, mob/user, params)
- if(!istype(weapon, /obj/item/device/cassette_tape))
- return
- var/obj/item/device/cassette_tape/attacked = weapon
- if(!attacked.approved_tape)
- to_chat(user, span_warning("The [src] smartly rejects the bootleg cassette tape"))
- return
- if(!inserted_tape)
- insert_tape(attacked)
- else
- if(!broadcasting)
- if(Adjacent(user) && !issiliconoradminghost(user))
- if(!user.put_in_hands(inserted_tape))
- inserted_tape.forceMove(drop_location())
- else
- inserted_tape.forceMove(drop_location())
- inserted_tape = null
- time_left = 0
- current_song_duration = 0
- pl_index = 0
- current_playlist = list()
- current_namelist = list()
- insert_tape(attacked)
- if(broadcasting)
- stop_broadcast(TRUE)
-
-/obj/machinery/cassette/dj_station/proc/insert_tape(obj/item/device/cassette_tape/CTape)
- if(inserted_tape || !istype(CTape))
- return
-
- inserted_tape = CTape
- CTape.forceMove(src)
-
- update_appearance()
- pl_index = 1
- if(inserted_tape.songs["side1"] && inserted_tape.songs["side2"])
- var/list/list = inserted_tape.songs["[inserted_tape.flipped ? "side2" : "side1"]"]
- for(var/song in list)
- current_playlist += song
-
- var/list/name_list = inserted_tape.song_names["[inserted_tape.flipped ? "side2" : "side1"]"]
- for(var/song in name_list)
- current_namelist += song
-
-/obj/machinery/cassette/dj_station/proc/stop_broadcast(soft = FALSE)
- STOP_PROCESSING(SSprocessing, src)
- GLOB.dj_broadcast = FALSE
- broadcasting = FALSE
- message_admins("[src] has stopped broadcasting [inserted_tape].")
- logger.Log(LOG_CATEGORY_MUSIC, "[src] has stopped broadcasting [inserted_tape]")
- for(var/client/anything as anything in active_listeners)
- if(!istype(anything))
- continue
- anything.tgui_panel?.stop_music()
- GLOB.youtube_exempt["dj-station"] -= anything
- active_listeners = list()
-
- if(!soft)
- for(var/mob/living/carbon/anything as anything in people_with_signals)
- if(!istype(anything))
- continue
- UnregisterSignal(anything, COMSIG_CARBON_UNEQUIP_EARS)
- UnregisterSignal(anything, COMSIG_CARBON_EQUIP_EARS)
- UnregisterSignal(anything, COMSIG_MOVABLE_Z_CHANGED)
- people_with_signals = list()
-
-/obj/machinery/cassette/dj_station/proc/start_broadcast()
- var/choice = tgui_input_list(usr, "Choose which song to play.", "[src]", current_namelist)
- if(!choice)
- return
- var/list_index = current_namelist.Find(choice)
- if(!list_index)
- return
- GLOB.dj_broadcast = TRUE
- pl_index = list_index
-
- var/list/viable_z = SSmapping.levels_by_any_trait(list(ZTRAIT_STATION, ZTRAIT_MINING, ZTRAIT_CENTCOM, ZTRAIT_RESERVED))
- for(var/mob/person as anything in GLOB.player_list)
- if(issilicon(person) || isobserver(person) || isaicamera(person) || isbot(person))
- active_listeners |= person.client
- continue
- if(iscarbon(person))
- var/mob/living/carbon/anything = person
- if(!(anything in people_with_signals))
- if(!istype(anything))
- continue
-
- RegisterSignal(anything, COMSIG_CARBON_UNEQUIP_EARS, PROC_REF(stop_solo_broadcast))
- RegisterSignal(anything, COMSIG_CARBON_EQUIP_EARS, PROC_REF(check_solo_broadcast))
- RegisterSignal(anything, COMSIG_MOVABLE_Z_CHANGED, PROC_REF(check_solo_broadcast))
- people_with_signals |= anything
-
- if(!(anything.client in active_listeners))
- if(!(anything.z in viable_z))
- continue
-
- if(!anything.client)
- continue
-
- if(anything.client in GLOB.youtube_exempt["walkman"])
- continue
-
- var/obj/item/ear_slot = anything.get_item_by_slot(ITEM_SLOT_EARS)
- if(istype(ear_slot, /obj/item/clothing/ears))
- var/obj/item/clothing/ears/worn
- if(!worn || !worn?.radio_compat)
- continue
- else if(!istype(ear_slot, /obj/item/radio/headset))
- continue
-
- if(!anything.client.prefs?.read_preference(/datum/preference/toggle/hear_music))
- continue
-
- active_listeners |= anything.client
-
- if(!length(active_listeners))
- return
-
- start_playing(active_listeners)
- START_PROCESSING(SSprocessing, src)
-
-
-/obj/machinery/cassette/dj_station/proc/check_solo_broadcast(mob/living/carbon/source, obj/item/clothing/ears/ear_item)
- SIGNAL_HANDLER
-
- if(!istype(source))
- return
-
- if(istype(ear_item, /obj/item/clothing/ears))
- var/obj/item/clothing/ears/worn
- if(!worn || !worn?.radio_compat)
- return
- else if(!istype(ear_item, /obj/item/radio/headset))
- return
-
- var/list/viable_z = SSmapping.levels_by_any_trait(list(ZTRAIT_STATION, ZTRAIT_MINING, ZTRAIT_CENTCOM))
- if(!(source.z in viable_z) || !source.client)
- return
-
- if(!source.client.prefs?.read_preference(/datum/preference/toggle/hear_music))
- return
-
- active_listeners |= source.client
- GLOB.youtube_exempt["dj-station"] |= source.client
- INVOKE_ASYNC(src, PROC_REF(start_playing),list(source.client))
-
-/obj/machinery/cassette/dj_station/proc/stop_solo_broadcast(mob/living/carbon/source)
- SIGNAL_HANDLER
-
- if(!source.client || !(source.client in active_listeners))
- return
-
- active_listeners -= source.client
- GLOB.youtube_exempt["dj-station"] -= source.client
- source.client.tgui_panel?.stop_music()
-
-/obj/machinery/cassette/dj_station/proc/start_playing(list/clients)
- if(!inserted_tape)
- if(broadcasting)
- stop_broadcast(TRUE)
- return
-
- waiting_for_yield = TRUE
- if(findtext(current_playlist[pl_index], GLOB.is_http_protocol))
- ///invoking youtube-dl
- var/ytdl = CONFIG_GET(string/invoke_youtubedl)
- ///the input for ytdl handled by the song list
- var/web_sound_input
- ///the url for youtube-dl
- var/web_sound_url = ""
- ///all extra data from the youtube-dl really want the name
- var/list/music_extra_data = list()
- web_sound_input = trim(current_playlist[pl_index])
- if(!(web_sound_input in GLOB.parsed_audio))
- ///scrubbing the input before putting it in the shell
- var/shell_scrubbed_input = shell_url_scrub(web_sound_input)
- ///putting it in the shell
- var/list/output = world.shelleo("[ytdl] --geo-bypass --format \"bestaudio\[ext=mp3]/best\[ext=mp4]\[height <= 360]/bestaudio\[ext=m4a]/bestaudio\[ext=aac]\" --dump-single-json --no-playlist --extractor-args \"youtube:lang=en\" -- \"[shell_scrubbed_input]\"")
- ///any errors
- var/errorlevel = output[SHELLEO_ERRORLEVEL]
- ///the standard output
- var/stdout = output[SHELLEO_STDOUT]
- if(!errorlevel)
- ///list for all the output data to go to
- var/list/data
- try
- data = json_decode(stdout)
- catch(var/exception/error) ///catch errors here
- to_chat(src, "Youtube-dl JSON parsing FAILED:", confidential = TRUE)
- to_chat(src, "[error]: [stdout]", confidential = TRUE)
- return
-
- if (data["url"])
- web_sound_url = data["url"]
- music_extra_data["start"] = data["start_time"]
- music_extra_data["end"] = data["end_time"]
- music_extra_data["link"] = data["webpage_url"]
- music_extra_data["title"] = data["title"]
- if(music_extra_data["start"])
- time_left = data["duration"] - music_extra_data["start"]
- else
- time_left = data["duration"]
-
- current_song_duration = data["duration"]
-
- GLOB.parsed_audio["[web_sound_input]"] = data
- else
- var/list/data = GLOB.parsed_audio["[web_sound_input]"]
- web_sound_url = data["url"]
- music_extra_data["start"] = data["start_time"]
- music_extra_data["end"] = data["end_time"]
- music_extra_data["link"] = data["webpage_url"]
- music_extra_data["title"] = data["title"]
- if(time_left <= 0)
- if(music_extra_data["start"])
- time_left = data["duration"] - music_extra_data["start"]
- else
- time_left = data["duration"]
-
- current_song_duration = data["duration"]
- music_extra_data["duration"] = data["duration"]
-
- if(time_left > 0)
- music_extra_data["start"] = music_extra_data["duration"] - time_left
-
- for(var/client/anything as anything in clients)
- if(!istype(anything))
- continue
- anything.tgui_panel?.play_music(web_sound_url, music_extra_data)
- GLOB.youtube_exempt["dj-station"] |= anything
- broadcasting = TRUE
- waiting_for_yield = FALSE
-
-/obj/machinery/cassette/dj_station/proc/add_new_player(mob/living/carbon/new_player)
- if(!(new_player in people_with_signals))
- RegisterSignal(new_player, COMSIG_CARBON_UNEQUIP_EARS, PROC_REF(stop_solo_broadcast))
- RegisterSignal(new_player, COMSIG_CARBON_EQUIP_EARS, PROC_REF(check_solo_broadcast))
- RegisterSignal(new_player, COMSIG_MOVABLE_Z_CHANGED, PROC_REF(check_solo_broadcast))
- people_with_signals |= new_player
-
- if(!broadcasting)
- return
-
- var/obj/item/ear_slot = new_player.get_item_by_slot(ITEM_SLOT_EARS)
- if(istype(ear_slot, /obj/item/clothing/ears))
- var/obj/item/clothing/ears/worn
- if(!worn || !worn?.radio_compat)
- return
- else if(!istype(ear_slot, /obj/item/radio/headset))
- return
- var/list/viable_z = SSmapping.levels_by_any_trait(list(ZTRAIT_STATION, ZTRAIT_MINING, ZTRAIT_CENTCOM))
- if(!(new_player.z in viable_z))
- return
-
- if(!(new_player.client in active_listeners))
- active_listeners |= new_player.client
- start_playing(list(new_player.client))
-
-/obj/machinery/cassette/dj_station/proc/next_song()
- waiting_for_yield = TRUE
- var/choice = tgui_input_number(usr, "Choose which song number to play.", "[src]", 1, length(current_playlist), 1)
- if(!choice)
- waiting_for_yield = FALSE
- stop_broadcast()
- return
- GLOB.dj_broadcast = TRUE
- pl_index = choice
-
- pl_index++
- start_playing(active_listeners)
+*/
diff --git a/monkestation/code/modules/cassettes/machines/dj_station_old.dm b/monkestation/code/modules/cassettes/machines/dj_station_old.dm
new file mode 100644
index 000000000000..f5ac8d864cbe
--- /dev/null
+++ b/monkestation/code/modules/cassettes/machines/dj_station_old.dm
@@ -0,0 +1,377 @@
+GLOBAL_VAR(dj_broadcast)
+GLOBAL_DATUM(dj_booth, /obj/machinery/cassette/dj_station)
+
+/obj/item/clothing/ears
+ //can we be used to listen to radio?
+ var/radio_compat = FALSE
+
+/obj/machinery/cassette/dj_station
+ name = "Cassette Player"
+ desc = "Plays Space Music Board approved cassettes for anyone in the station to listen to."
+
+ icon = 'monkestation/code/modules/cassettes/icons/radio_station.dmi'
+ icon_state = "cassette_player"
+
+ active_power_usage = BASE_MACHINE_ACTIVE_CONSUMPTION
+
+ resistance_flags = INDESTRUCTIBLE
+ anchored = TRUE
+ density = TRUE
+ var/broadcasting = FALSE
+ var/obj/item/device/cassette_tape/inserted_tape
+ var/time_left = 0
+ var/current_song_duration = 0
+ var/list/people_with_signals = list()
+ var/list/active_listeners = list()
+ var/waiting_for_yield = FALSE
+
+ //tape stuff goes here
+ var/pl_index = 0
+ var/list/current_playlist = list()
+ var/list/current_namelist = list()
+
+ COOLDOWN_DECLARE(next_song_timer)
+
+/obj/machinery/cassette/dj_station/Initialize(mapload)
+ . = ..()
+ REGISTER_REQUIRED_MAP_ITEM(1, INFINITY)
+ register_context()
+ if(QDELETED(GLOB.dj_booth))
+ GLOB.dj_booth = src
+
+/obj/machinery/cassette/dj_station/Destroy()
+ if(GLOB.dj_booth == src)
+ GLOB.dj_booth = null
+ return ..()
+
+/obj/machinery/cassette/dj_station/add_context(atom/source, list/context, obj/item/held_item, mob/user)
+ . = ..()
+ if(inserted_tape)
+ context[SCREENTIP_CONTEXT_CTRL_LMB] = "Eject Tape"
+ if(!broadcasting)
+ context[SCREENTIP_CONTEXT_LMB] = "Play Tape"
+ return CONTEXTUAL_SCREENTIP_SET
+
+/obj/machinery/cassette/dj_station/examine(mob/user)
+ . = ..()
+ if(time_left > 0 || next_song_timer)
+ . += span_notice("It seems to be cooling down, you estimate it will take about [time_left ? DisplayTimeText(((time_left * 10) + 6000)) : DisplayTimeText(COOLDOWN_TIMELEFT(src, next_song_timer))].")
+
+/obj/machinery/cassette/dj_station/process(seconds_per_tick)
+ if(waiting_for_yield)
+ return
+ time_left -= round(seconds_per_tick)
+ if(time_left <= 0)
+ time_left = 0
+ if(COOLDOWN_FINISHED(src, next_song_timer) && broadcasting)
+ COOLDOWN_START(src, next_song_timer, 10 MINUTES)
+ broadcasting = FALSE
+
+/obj/machinery/cassette/dj_station/attack_hand(mob/user)
+ . = ..()
+ if(!inserted_tape)
+ return
+ if((!COOLDOWN_FINISHED(src, next_song_timer)) && !broadcasting)
+ to_chat(user, span_notice("The [src] feels hot to the touch and needs time to cooldown."))
+ to_chat(user, span_info("You estimate it will take about [time_left ? DisplayTimeText(((time_left * 10) + 6000)) : DisplayTimeText(COOLDOWN_TIMELEFT(src, next_song_timer))] to cool down."))
+ return
+ message_admins("[src] started broadcasting [inserted_tape] interacted with by [user]")
+ logger.Log(LOG_CATEGORY_MUSIC, "[src] started broadcasting [inserted_tape]")
+ start_broadcast()
+
+/obj/machinery/cassette/dj_station/AltClick(mob/user)
+ . = ..()
+ if(!isliving(user) || !user.Adjacent(src))
+ return
+ if(!inserted_tape)
+ return
+ if(broadcasting)
+ next_song()
+
+/obj/machinery/cassette/dj_station/CtrlClick(mob/user)
+ . = ..()
+ if(!inserted_tape || broadcasting)
+ return
+ if(Adjacent(user) && !issiliconoradminghost(user))
+ if(!user.put_in_hands(inserted_tape))
+ inserted_tape.forceMove(drop_location())
+ else
+ inserted_tape.forceMove(drop_location())
+ inserted_tape = null
+ time_left = 0
+ current_song_duration = 0
+ pl_index = 0
+ current_playlist = list()
+ current_namelist = list()
+ stop_broadcast(TRUE)
+
+/obj/machinery/cassette/dj_station/attackby(obj/item/weapon, mob/user, params)
+ if(!istype(weapon, /obj/item/device/cassette_tape))
+ return
+ var/obj/item/device/cassette_tape/attacked = weapon
+ if(!attacked.approved_tape)
+ to_chat(user, span_warning("The [src] smartly rejects the bootleg cassette tape"))
+ return
+ if(!inserted_tape)
+ insert_tape(attacked)
+ else
+ if(!broadcasting)
+ if(Adjacent(user) && !issiliconoradminghost(user))
+ if(!user.put_in_hands(inserted_tape))
+ inserted_tape.forceMove(drop_location())
+ else
+ inserted_tape.forceMove(drop_location())
+ inserted_tape = null
+ time_left = 0
+ current_song_duration = 0
+ pl_index = 0
+ current_playlist = list()
+ current_namelist = list()
+ insert_tape(attacked)
+ if(broadcasting)
+ stop_broadcast(TRUE)
+
+/obj/machinery/cassette/dj_station/proc/insert_tape(obj/item/device/cassette_tape/CTape)
+ if(inserted_tape || !istype(CTape))
+ return
+
+ inserted_tape = CTape
+ CTape.forceMove(src)
+
+ update_appearance()
+ pl_index = 1
+ if(inserted_tape.songs["side1"] && inserted_tape.songs["side2"])
+ var/list/list = inserted_tape.songs["[inserted_tape.flipped ? "side2" : "side1"]"]
+ for(var/song in list)
+ current_playlist += song
+
+ var/list/name_list = inserted_tape.song_names["[inserted_tape.flipped ? "side2" : "side1"]"]
+ for(var/song in name_list)
+ current_namelist += song
+
+/obj/machinery/cassette/dj_station/proc/stop_broadcast(soft = FALSE)
+ STOP_PROCESSING(SSprocessing, src)
+ GLOB.dj_broadcast = FALSE
+ broadcasting = FALSE
+ message_admins("[src] has stopped broadcasting [inserted_tape].")
+ logger.Log(LOG_CATEGORY_MUSIC, "[src] has stopped broadcasting [inserted_tape]")
+ for(var/client/anything as anything in active_listeners)
+ if(!istype(anything))
+ continue
+ anything.tgui_panel?.stop_music()
+ GLOB.youtube_exempt["dj-station"] -= anything
+ active_listeners = list()
+
+ if(!soft)
+ for(var/mob/living/carbon/anything as anything in people_with_signals)
+ if(!istype(anything))
+ continue
+ UnregisterSignal(anything, COMSIG_CARBON_UNEQUIP_EARS)
+ UnregisterSignal(anything, COMSIG_CARBON_EQUIP_EARS)
+ UnregisterSignal(anything, COMSIG_MOVABLE_Z_CHANGED)
+ people_with_signals = list()
+
+/obj/machinery/cassette/dj_station/proc/start_broadcast()
+ var/choice = tgui_input_list(usr, "Choose which song to play.", "[src]", current_namelist)
+ if(!choice)
+ return
+ var/list_index = current_namelist.Find(choice)
+ if(!list_index)
+ return
+ GLOB.dj_broadcast = TRUE
+ pl_index = list_index
+
+ var/list/viable_z = SSmapping.levels_by_any_trait(list(ZTRAIT_STATION, ZTRAIT_MINING, ZTRAIT_CENTCOM, ZTRAIT_RESERVED))
+ for(var/mob/person as anything in GLOB.player_list)
+ if(issilicon(person) || isobserver(person) || isaicamera(person) || isbot(person))
+ active_listeners |= person.client
+ continue
+ if(iscarbon(person))
+ var/mob/living/carbon/anything = person
+ if(!(anything in people_with_signals))
+ if(!istype(anything))
+ continue
+
+ RegisterSignal(anything, COMSIG_CARBON_UNEQUIP_EARS, PROC_REF(stop_solo_broadcast))
+ RegisterSignal(anything, COMSIG_CARBON_EQUIP_EARS, PROC_REF(check_solo_broadcast))
+ RegisterSignal(anything, COMSIG_MOVABLE_Z_CHANGED, PROC_REF(check_solo_broadcast))
+ people_with_signals |= anything
+
+ if(!(anything.client in active_listeners))
+ if(!(anything.z in viable_z))
+ continue
+
+ if(!anything.client)
+ continue
+
+ if(anything.client in GLOB.youtube_exempt["walkman"])
+ continue
+
+ var/obj/item/ear_slot = anything.get_item_by_slot(ITEM_SLOT_EARS)
+ if(istype(ear_slot, /obj/item/clothing/ears))
+ var/obj/item/clothing/ears/worn
+ if(!worn || !worn?.radio_compat)
+ continue
+ else if(!istype(ear_slot, /obj/item/radio/headset))
+ continue
+
+ if(!anything.client.prefs?.read_preference(/datum/preference/toggle/hear_music))
+ continue
+
+ active_listeners |= anything.client
+
+ if(!length(active_listeners))
+ return
+
+ start_playing(active_listeners)
+ START_PROCESSING(SSprocessing, src)
+
+
+/obj/machinery/cassette/dj_station/proc/check_solo_broadcast(mob/living/carbon/source, obj/item/clothing/ears/ear_item)
+ SIGNAL_HANDLER
+
+ if(!istype(source))
+ return
+
+ if(istype(ear_item, /obj/item/clothing/ears))
+ var/obj/item/clothing/ears/worn
+ if(!worn || !worn?.radio_compat)
+ return
+ else if(!istype(ear_item, /obj/item/radio/headset))
+ return
+
+ var/list/viable_z = SSmapping.levels_by_any_trait(list(ZTRAIT_STATION, ZTRAIT_MINING, ZTRAIT_CENTCOM))
+ if(!(source.z in viable_z) || !source.client)
+ return
+
+ if(!source.client.prefs?.read_preference(/datum/preference/toggle/hear_music))
+ return
+
+ active_listeners |= source.client
+ GLOB.youtube_exempt["dj-station"] |= source.client
+ INVOKE_ASYNC(src, PROC_REF(start_playing),list(source.client))
+
+/obj/machinery/cassette/dj_station/proc/stop_solo_broadcast(mob/living/carbon/source)
+ SIGNAL_HANDLER
+
+ if(!source.client || !(source.client in active_listeners))
+ return
+
+ active_listeners -= source.client
+ GLOB.youtube_exempt["dj-station"] -= source.client
+ source.client.tgui_panel?.stop_music()
+
+/obj/machinery/cassette/dj_station/proc/start_playing(list/clients)
+ if(!inserted_tape)
+ if(broadcasting)
+ stop_broadcast(TRUE)
+ return
+
+ waiting_for_yield = TRUE
+ if(is_http_protocol(current_playlist[pl_index]))
+ ///invoking youtube-dl
+ var/ytdl = CONFIG_GET(string/invoke_youtubedl)
+ ///the input for ytdl handled by the song list
+ var/web_sound_input
+ ///the url for youtube-dl
+ var/web_sound_url = ""
+ ///all extra data from the youtube-dl really want the name
+ var/list/music_extra_data = list()
+ web_sound_input = trim(current_playlist[pl_index])
+ if(!(web_sound_input in GLOB.parsed_audio))
+ ///scrubbing the input before putting it in the shell
+ var/shell_scrubbed_input = shell_url_scrub(web_sound_input)
+ ///putting it in the shell
+ var/list/output = world.shelleo("[ytdl] --geo-bypass --format \"bestaudio\[ext=mp3]/best\[ext=mp4]\[height <= 360]/bestaudio\[ext=m4a]/bestaudio\[ext=aac]\" --dump-single-json --no-playlist --extractor-args \"youtube:lang=en\" -- \"[shell_scrubbed_input]\"")
+ ///any errors
+ var/errorlevel = output[SHELLEO_ERRORLEVEL]
+ ///the standard output
+ var/stdout = output[SHELLEO_STDOUT]
+ if(!errorlevel)
+ ///list for all the output data to go to
+ var/list/data
+ try
+ data = json_decode(stdout)
+ catch(var/exception/error) ///catch errors here
+ to_chat(src, "Youtube-dl JSON parsing FAILED:", confidential = TRUE)
+ to_chat(src, "[error]: [stdout]", confidential = TRUE)
+ return
+
+ if (data["url"])
+ web_sound_url = data["url"]
+ music_extra_data["start"] = data["start_time"]
+ music_extra_data["end"] = data["end_time"]
+ music_extra_data["link"] = data["webpage_url"]
+ music_extra_data["title"] = data["title"]
+ if(music_extra_data["start"])
+ time_left = data["duration"] - music_extra_data["start"]
+ else
+ time_left = data["duration"]
+
+ current_song_duration = data["duration"]
+
+ GLOB.parsed_audio["[web_sound_input]"] = data
+ else
+ var/list/data = GLOB.parsed_audio["[web_sound_input]"]
+ web_sound_url = data["url"]
+ music_extra_data["start"] = data["start_time"]
+ music_extra_data["end"] = data["end_time"]
+ music_extra_data["link"] = data["webpage_url"]
+ music_extra_data["title"] = data["title"]
+ if(time_left <= 0)
+ if(music_extra_data["start"])
+ time_left = data["duration"] - music_extra_data["start"]
+ else
+ time_left = data["duration"]
+
+ current_song_duration = data["duration"]
+ music_extra_data["duration"] = data["duration"]
+
+ if(time_left > 0)
+ music_extra_data["start"] = music_extra_data["duration"] - time_left
+
+ for(var/client/anything as anything in clients)
+ if(!istype(anything))
+ continue
+ anything.tgui_panel?.play_music(web_sound_url, music_extra_data)
+ GLOB.youtube_exempt["dj-station"] |= anything
+ broadcasting = TRUE
+ waiting_for_yield = FALSE
+
+/obj/machinery/cassette/dj_station/proc/add_new_player(mob/living/carbon/new_player)
+ if(!(new_player in people_with_signals))
+ RegisterSignal(new_player, COMSIG_CARBON_UNEQUIP_EARS, PROC_REF(stop_solo_broadcast))
+ RegisterSignal(new_player, COMSIG_CARBON_EQUIP_EARS, PROC_REF(check_solo_broadcast))
+ RegisterSignal(new_player, COMSIG_MOVABLE_Z_CHANGED, PROC_REF(check_solo_broadcast))
+ people_with_signals |= new_player
+
+ if(!broadcasting)
+ return
+
+ var/obj/item/ear_slot = new_player.get_item_by_slot(ITEM_SLOT_EARS)
+ if(istype(ear_slot, /obj/item/clothing/ears))
+ var/obj/item/clothing/ears/worn
+ if(!worn || !worn?.radio_compat)
+ return
+ else if(!istype(ear_slot, /obj/item/radio/headset))
+ return
+ var/list/viable_z = SSmapping.levels_by_any_trait(list(ZTRAIT_STATION, ZTRAIT_MINING, ZTRAIT_CENTCOM))
+ if(!(new_player.z in viable_z))
+ return
+
+ if(!(new_player.client in active_listeners))
+ active_listeners |= new_player.client
+ start_playing(list(new_player.client))
+
+/obj/machinery/cassette/dj_station/proc/next_song()
+ waiting_for_yield = TRUE
+ var/choice = tgui_input_number(usr, "Choose which song number to play.", "[src]", 1, length(current_playlist), 1)
+ if(!choice)
+ waiting_for_yield = FALSE
+ stop_broadcast()
+ return
+ GLOB.dj_broadcast = TRUE
+ pl_index = choice
+
+ pl_index++
+ start_playing(active_listeners)
diff --git a/monkestation/code/modules/cassettes/walkman/_walkmen.dm b/monkestation/code/modules/cassettes/walkman/_walkmen.dm
index 6eee606c9ff9..9e8bc846a3b7 100644
--- a/monkestation/code/modules/cassettes/walkman/_walkmen.dm
+++ b/monkestation/code/modules/cassettes/walkman/_walkmen.dm
@@ -142,7 +142,7 @@ GLOBAL_LIST_INIT(youtube_exempt, list(
/obj/item/device/walkman/proc/play()
if(!current_song)
if(current_playlist.len > 0)
- if(findtext(current_playlist[pl_index], GLOB.is_http_protocol))
+ if(is_http_protocol(current_playlist[pl_index]))
///invoking youtube-dl
var/ytdl = CONFIG_GET(string/invoke_youtubedl)
///the input for ytdl handled by the song list
@@ -285,7 +285,7 @@ GLOBAL_LIST_INIT(youtube_exempt, list(
break_sound()
pl_index = pl_index + 1 <= current_playlist.len ? (pl_index += 1) : 1
- link_play = findtext(current_playlist[pl_index], GLOB.is_http_protocol) ? TRUE : FALSE
+ link_play = is_http_protocol(current_playlist[pl_index])
if(!link_play)
diff --git a/tgstation.dme b/tgstation.dme
index 7518b42b14c8..9bb4c1954f89 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -608,6 +608,7 @@
#include "code\__HELPERS\~monkestation-helpers\mobs.dm"
#include "code\__HELPERS\~monkestation-helpers\records.dm"
#include "code\__HELPERS\~monkestation-helpers\roundend.dm"
+#include "code\__HELPERS\~monkestation-helpers\text.dm"
#include "code\__HELPERS\~monkestation-helpers\time.dm"
#include "code\__HELPERS\~monkestation-helpers\virology.dm"
#include "code\__HELPERS\~monkestation-helpers\logging\attack.dm"