From c88299f05ce283677ea050e9e92c5705dbab5b97 Mon Sep 17 00:00:00 2001 From: Lucy Date: Sun, 15 Dec 2024 02:54:00 -0500 Subject: [PATCH] [ci skip] Refactors cassettes and the cassette player --- code/__HELPERS/~monkestation-helpers/text.dm | 6 + code/_globalvars/_regexes.dm | 3 - code/modules/admin/verbs/playsound.dm | 4 +- code/modules/requests/request_manager.dm | 2 +- .../cassettes/cassette_db/cassette_datum.dm | 129 +++--- .../modules/cassettes/machines/dj_station.dm | 359 +---------------- .../cassettes/machines/dj_station_old.dm | 377 ++++++++++++++++++ .../modules/cassettes/walkman/_walkmen.dm | 4 +- tgstation.dme | 1 + 9 files changed, 473 insertions(+), 412 deletions(-) create mode 100644 code/__HELPERS/~monkestation-helpers/text.dm create mode 100644 monkestation/code/modules/cassettes/machines/dj_station_old.dm 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"