diff --git a/code/__DEFINES/admin.dm b/code/__DEFINES/admin.dm index 87fc73b2278..6c65c1ae747 100644 --- a/code/__DEFINES/admin.dm +++ b/code/__DEFINES/admin.dm @@ -6,6 +6,7 @@ #define MUTE_PRAY (1<<2) #define MUTE_ADMINHELP (1<<3) #define MUTE_DEADCHAT (1<<4) +#define MUTE_INTERNET_REQUEST (1<<5) #define MUTE_ALL (~0) //Some constants for DB_Ban @@ -70,6 +71,8 @@ #define ADMIN_LUAVIEW_CHUNK(state, log_index) "(VIEW CODE)" /// Displays "(SHOW)" in the chat, when clicked it tries to show atom(paper). First you need to set the request_state variable to TRUE for the paper. #define ADMIN_SHOW_PAPER(atom) "(SHOW)" +/// Displays "(PLAY)" in the chat, when clicked it tries to play internet sounds from the request. +#define ADMIN_PLAY_INTERNET(text) "(PLAY)" /atom/proc/Admin_Coordinates_Readable(area_name, admin_jump_ref) var/turf/T = Safe_COORD_Location() diff --git a/code/__HELPERS/logging/game.dm b/code/__HELPERS/logging/game.dm index 0bf180e1842..40fd458d1a9 100644 --- a/code/__HELPERS/logging/game.dm +++ b/code/__HELPERS/logging/game.dm @@ -23,6 +23,11 @@ if (CONFIG_GET(flag/log_prayer)) WRITE_LOG(GLOB.world_game_log, "PRAY: [text]") +/// Logging for music requests +/proc/log_internet_request(text) + if (CONFIG_GET(flag/log_internet_request)) + WRITE_LOG(GLOB.world_game_log, "INTERNET REQUEST: [text]") + /// Logging for logging in & out of the game, with error messages. /proc/log_access(text) if (CONFIG_GET(flag/log_access)) diff --git a/code/controllers/configuration/entries/general.dm b/code/controllers/configuration/entries/general.dm index f6a109df72b..e79fd0f6fc1 100644 --- a/code/controllers/configuration/entries/general.dm +++ b/code/controllers/configuration/entries/general.dm @@ -76,6 +76,9 @@ /// log prayers /datum/config_entry/flag/log_prayer +///Log Music Requests +/datum/config_entry/flag/log_internet_request + /// log silicons /datum/config_entry/flag/log_silicon @@ -354,6 +357,11 @@ /datum/config_entry/string/invoke_youtubedl protection = CONFIG_ENTRY_LOCKED | CONFIG_ENTRY_HIDDEN +/datum/config_entry/flag/request_internet_sound + +/datum/config_entry/string/request_internet_allowed + protection = CONFIG_ENTRY_LOCKED + /datum/config_entry/flag/show_irc_name /datum/config_entry/flag/no_default_techweb_link diff --git a/code/modules/admin/topic.dm b/code/modules/admin/topic.dm index 085f53b5350..8d7aead7eb8 100644 --- a/code/modules/admin/topic.dm +++ b/code/modules/admin/topic.dm @@ -1748,3 +1748,13 @@ if(!paper_to_show) return paper_to_show.ui_interact(usr) + else if(href_list["play_internet"]) + if(!check_rights(R_SOUND)) + return + + var/link_url = href_list["play_internet"] + if(!link_url) + return + + web_sound(usr, link_url) + diff --git a/code/modules/admin/verbs/admin.dm b/code/modules/admin/verbs/admin.dm index 2558606499a..c7f3c740f83 100644 --- a/code/modules/admin/verbs/admin.dm +++ b/code/modules/admin/verbs/admin.dm @@ -208,6 +208,9 @@ if(MUTE_DEADCHAT) mute_string = "deadchat and DSAY" feedback_string = "Deadchat" + if(MUTE_INTERNET_REQUEST) + mute_string = "internet sound requests" + feedback_string = "Internet Sound Requests" if(MUTE_ALL) mute_string = "everything" feedback_string = "Everything" diff --git a/code/modules/admin/verbs/admingame.dm b/code/modules/admin/verbs/admingame.dm index 21d17a05429..228e994a0c1 100644 --- a/code/modules/admin/verbs/admingame.dm +++ b/code/modules/admin/verbs/admingame.dm @@ -84,6 +84,7 @@ body += "OOC | " body += "PRAY | " body += "ADMINHELP | " + body += "WEBREQ | " body += "DEADCHAT\]" body += "(toggle all)" diff --git a/code/modules/admin/verbs/playsound.dm b/code/modules/admin/verbs/playsound.dm index b1031a38fa1..0b0fbaf0f58 100644 --- a/code/modules/admin/verbs/playsound.dm +++ b/code/modules/admin/verbs/playsound.dm @@ -70,6 +70,100 @@ SEND_SOUND(M, S) SSblackbox.record_feedback("tally", "admin_verb", 1, "Play Direct Mob Sound") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! +///Takes an input from either proc/play_web_sound or the request manager and runs it through youtube-dl and prompts the user before playing it to the server. +/proc/web_sound(mob/user, input) + if(!check_rights(R_SOUND)) + return + var/ytdl = CONFIG_GET(string/invoke_youtubedl) + if(!ytdl) + to_chat(user, span_boldwarning("Youtube-dl was not configured, action unavailable"), confidential = TRUE) //Check config.txt for the INVOKE_YOUTUBEDL value + return + var/web_sound_url = "" + var/stop_web_sounds = FALSE + var/list/music_extra_data = list() + if(istext(input)) + 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 -- \"[input]\"") + var/errorlevel = output[SHELLEO_ERRORLEVEL] + var/stdout = output[SHELLEO_STDOUT] + var/stderr = output[SHELLEO_STDERR] + if(errorlevel) + to_chat(user, span_boldwarning("Youtube-dl URL retrieval FAILED:"), confidential = TRUE) + to_chat(user, span_warning("[stderr]"), confidential = TRUE) + return + var/list/data + try + data = json_decode(stdout) + catch(var/exception/e) + to_chat(user, span_boldwarning("Youtube-dl JSON parsing FAILED:"), confidential = TRUE) + to_chat(user, span_warning("[e]: [stdout]"), confidential = TRUE) + return + if (data["url"]) + web_sound_url = data["url"] + var/title = "[data["title"]]" + var/webpage_url = title + if (data["webpage_url"]) + webpage_url = "[title]" + music_extra_data["duration"] = DisplayTimeText(data["duration"] * 1 SECONDS) + music_extra_data["link"] = data["webpage_url"] + music_extra_data["artist"] = data["artist"] + music_extra_data["upload_date"] = data["upload_date"] + music_extra_data["album"] = data["album"] + var/duration = data["duration"] * 1 SECONDS + if (duration > 10 MINUTES) + if((tgui_alert(user, "This song is over 10 minutes long. Are you sure you want to play it?", "Length Warning!", list("No", "Yes", "Cancel")) != "Yes")) + return + var/res = tgui_alert(user, "Show the title of and link to this song to the players?\n[title]", "Show Info?", list("Yes", "No", "Cancel")) + switch(res) + if("Yes") + music_extra_data["title"] = data["title"] + if("No") + music_extra_data["link"] = "Song Link Hidden" + music_extra_data["title"] = "Song Title Hidden" + music_extra_data["artist"] = "Song Artist Hidden" + music_extra_data["upload_date"] = "Song Upload Date Hidden" + music_extra_data["album"] = "Song Album Hidden" + if("Cancel", null) + return + var/anon = tgui_alert(user, "Display who played the song?", "Credit Yourself?", list("Yes", "No", "Cancel")) + switch(anon) + if("Yes") + if(res == "Yes") + to_chat(world, span_boldannounce("[user] played: [webpage_url]"), confidential = TRUE) + else + to_chat(world, span_boldannounce("[user] played some music"), confidential = TRUE) + if("No") + if(res == "Yes") + to_chat(world, span_boldannounce("An admin played: [webpage_url]"), confidential = TRUE) + if("Cancel", null) + return + SSblackbox.record_feedback("nested tally", "played_url", 1, list("[user.ckey]", "[input]")) + log_admin("[key_name(user)] played web sound: [input]") + message_admins("[key_name(user)] played web sound: [input]") + else + //pressed ok with blank + log_admin("[key_name(user)] stopped web sounds.") + + 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)) + 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) + + return + if(web_sound_url || stop_web_sounds) + for(var/m in GLOB.player_list) + var/mob/M = m + var/client/C = M.client + if(C.prefs.read_preference(/datum/preference/toggle/sound_midi)) + if(!stop_web_sounds) + C.tgui_panel?.play_music(web_sound_url, music_extra_data) + else + C.tgui_panel?.stop_music() + + SSblackbox.record_feedback("tally", "admin_verb", 1, "Play Internet Sound") + + /client/proc/play_web_sound() set category = "Admin.Fun" set name = "Play Internet Sound" @@ -81,98 +175,18 @@ to_chat(src, span_boldwarning("Youtube-dl was not configured, action unavailable"), confidential = TRUE) //Check config.txt for the INVOKE_YOUTUBEDL value return - var/web_sound_input = input("Enter content URL (supported sites only, leave blank to stop playing)", "Play Internet Sound via youtube-dl") as text|null - if(istext(web_sound_input)) - var/web_sound_url = "" - var/stop_web_sounds = FALSE - var/list/music_extra_data = list() - if(length(web_sound_input)) + var/web_sound_input = tgui_input_text(usr, "Enter content URL (supported sites only, leave blank to stop playing)", "Play Internet Sound", null) - web_sound_input = trim(web_sound_input) - if(findtext(web_sound_input, ":") && !findtext(web_sound_input, GLOB.is_http_protocol)) - 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 - var/shell_scrubbed_input = shell_url_scrub(web_sound_input) - 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 -- \"[shell_scrubbed_input]\"") - var/errorlevel = output[SHELLEO_ERRORLEVEL] - var/stdout = output[SHELLEO_STDOUT] - var/stderr = output[SHELLEO_STDERR] - if(!errorlevel) - var/list/data - try - data = json_decode(stdout) - catch(var/exception/e) - to_chat(src, span_boldwarning("Youtube-dl JSON parsing FAILED:"), confidential = TRUE) - to_chat(src, span_warning("[e]: [stdout]"), confidential = TRUE) - return - - if (data["url"]) - web_sound_url = data["url"] - var/title = "[data["title"]]" - var/webpage_url = title - if (data["webpage_url"]) - webpage_url = "[title]" - music_extra_data["duration"] = DisplayTimeText(data["duration"] * 1 SECONDS) - music_extra_data["link"] = data["webpage_url"] - music_extra_data["artist"] = data["artist"] - music_extra_data["upload_date"] = data["upload_date"] - music_extra_data["album"] = data["album"] - - var/res = tgui_alert(usr, "Show the title of and link to this song to the players?\n[title]",, list("No", "Yes", "Cancel")) - switch(res) - if("Yes") - music_extra_data["title"] = data["title"] - if("No") - music_extra_data["link"] = "Song Link Hidden" - music_extra_data["title"] = "Song Title Hidden" - music_extra_data["artist"] = "Song Artist Hidden" - music_extra_data["upload_date"] = "Song Upload Date Hidden" - music_extra_data["album"] = "Song Album Hidden" - if("Cancel") - return - - var/anon = tgui_alert(usr, "Display who played the song?", "Credit Yourself?", list("No", "Yes", "Cancel")) - switch(anon) - if("Yes") - if(res == "Yes") - to_chat(world, span_boldannounce("[src] played: [webpage_url]"), confidential = TRUE) - else - to_chat(world, span_boldannounce("[src] played some music"), confidential = TRUE) - if("No") - if(res == "Yes") - to_chat(world, span_boldannounce("An admin played: [webpage_url]"), confidential = TRUE) - if("Cancel") - return - - SSblackbox.record_feedback("nested tally", "played_url", 1, list("[ckey]", "[web_sound_input]")) - log_admin("[key_name(src)] played web sound: [web_sound_input]") - message_admins("[key_name(src)] played web sound: [web_sound_input]") - else - to_chat(src, span_boldwarning("Youtube-dl URL retrieval FAILED:"), confidential = TRUE) - to_chat(src, span_warning("[stderr]"), confidential = TRUE) - - else //pressed ok with blank - log_admin("[key_name(src)] stopped web sound") - message_admins("[key_name(src)] stopped web sound") - web_sound_url = null - stop_web_sounds = TRUE - - if(web_sound_url && !findtext(web_sound_url, GLOB.is_http_protocol)) - to_chat(src, span_boldwarning("BLOCKED: Content URL not using http(s) protocol"), confidential = TRUE) - to_chat(src, span_warning("The media provider returned a content URL that isn't using the HTTP or HTTPS protocol"), confidential = TRUE) + 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)) + 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 - if(web_sound_url || stop_web_sounds) - for(var/m in GLOB.player_list) - var/mob/M = m - var/client/C = M.client - if(C.prefs.read_preference(/datum/preference/toggle/sound_midi)) - if(!stop_web_sounds) - C.tgui_panel?.play_music(web_sound_url, music_extra_data) - else - C.tgui_panel?.stop_music() - - SSblackbox.record_feedback("tally", "admin_verb", 1, "Play Internet Sound") + var/shell_scrubbed_input = shell_url_scrub(web_sound_input) + web_sound(usr, shell_scrubbed_input) + else + web_sound(usr, null) /client/proc/set_round_end_sound(S as sound) set category = "Admin.Fun" diff --git a/code/modules/admin/verbs/request_internet_sound.dm b/code/modules/admin/verbs/request_internet_sound.dm new file mode 100644 index 00000000000..9b5cbbbf9f9 --- /dev/null +++ b/code/modules/admin/verbs/request_internet_sound.dm @@ -0,0 +1,39 @@ +/mob/verb/request_internet_sound() + set category = "OOC" + set name = "Request Internet Sound" + + if(GLOB.say_disabled) //This is here to try to identify lag problems + to_chat(usr, span_danger("Speech is currently admin-disabled."), confidential = TRUE) + return + + if (!CONFIG_GET(flag/request_internet_sound)) + to_chat(usr, span_danger("This server has disabled internet sound requests."), confidential = TRUE) + return + + var/request_url = tgui_input_text(usr, "Please Input a URL", "Only certain sites are allowed, such as YouTube, SoundCloud, and Bandcamp.", "") + if(!request_url) + return + + //regex filter + var/regex/allowed_regex = regex(replacetext(CONFIG_GET(string/request_internet_allowed), ",", "|"), "i") + if(!allowed_regex.Find(request_url)) + to_chat(usr, span_danger("Invalid URL. Please use a URL from one of the following sites: [replacetext(CONFIG_GET(string/request_internet_allowed), "\\", "")]"), confidential = TRUE) + return + + request_url = shell_url_scrub(request_url) + log_internet_request("[src.key]/([src.name]): [request_url]") + if(usr.client) + if(usr.client.prefs.muted & MUTE_INTERNET_REQUEST) + to_chat(usr, span_danger("You cannot request music at this time. (muted)."), confidential = TRUE) + return + if(src.client.handle_spam_prevention(request_url,MUTE_INTERNET_REQUEST)) + return + + GLOB.requests.music_request(usr.client, request_url) + to_chat(usr, span_info("You requested: \"[request_url]\" to be played."), confidential = TRUE) + request_url = span_adminnotice("MUSIC REQUEST: [ADMIN_FULLMONTY(src)] [ADMIN_SC(src)]: [span_linkify(request_url)] [ADMIN_PLAY_INTERNET(request_url)]") + for(var/client/admin_client in GLOB.admins) + if(admin_client.prefs.chat_toggles & CHAT_PRAYER) + to_chat(admin_client, request_url, type = MESSAGE_TYPE_PRAYER, confidential = TRUE) + + SSblackbox.record_feedback("tally", "music_request", 1, "Music Request") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! diff --git a/code/modules/requests/request_manager.dm b/code/modules/requests/request_manager.dm index fef7e9fb0dd..19a94a61dad 100644 --- a/code/modules/requests/request_manager.dm +++ b/code/modules/requests/request_manager.dm @@ -8,6 +8,8 @@ #define REQUEST_NUKE "request_nuke" /// Requests somebody from fax #define REQUEST_FAX "request_fax" +/// Requests from Request Music +#define REQUEST_INTERNET_SOUND "request_internet_sound" GLOBAL_DATUM_INIT(requests, /datum/request_manager, new) @@ -107,6 +109,17 @@ GLOBAL_DATUM_INIT(requests, /datum/request_manager, new) /datum/request_manager/proc/fax_request(client/requester, message, additional_info) request_for_client(requester, REQUEST_FAX, message, additional_info) +/** + * Creates a request for a song + * + * Arguments: + * * requester - The client who is sending the request + * * message - The URL of the song + */ + +/datum/request_manager/proc/music_request(client/requester, message) + request_for_client(requester, REQUEST_INTERNET_SOUND, message) + /** * Creates a request and registers the request with all necessary internal tracking lists * @@ -221,6 +234,16 @@ GLOBAL_DATUM_INIT(requests, /datum/request_manager, new) var/obj/item/paper/request_message = request.additional_information request_message.ui_interact(usr) return TRUE + if ("play") + 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)) + to_chat(usr, "Request is not a valid URL.", confidential = TRUE) + return TRUE + + web_sound(usr, request.message) + return TRUE /datum/request_manager/ui_data(mob/user) . = list( @@ -246,3 +269,4 @@ GLOBAL_DATUM_INIT(requests, /datum/request_manager, new) #undef REQUEST_SYNDICATE #undef REQUEST_NUKE #undef REQUEST_FAX +#undef REQUEST_INTERNET_SOUND diff --git a/config/config.txt b/config/config.txt index 9ee615a63ed..8a217d61292 100644 --- a/config/config.txt +++ b/config/config.txt @@ -130,6 +130,9 @@ LOG_TELECOMMS ## log prayers LOG_PRAYER +## Log Music requests +LOG_INTERNET_REQUEST + ## log silicons LOG_SILICON @@ -295,6 +298,14 @@ CHECK_RANDOMIZER ## The default value assumes youtube-dl is in your system PATH # INVOKE_YOUTUBEDL youtube-dl +## Comment this out to disable users ability to use the request internet sounds to be played. +REQUEST_INTERNET_SOUND + +## Request Internet Sound Allowed URL'S comma separated, urls here are the only sites allowed through Request Internet Sound, Add more to allow more, or remove to disallow. +## The Defaults here are all supported by youtube-dl +## Ensure . and / are escaped with \ +REQUEST_INTERNET_ALLOWED youtube\.com\/watch?v=,youtu\.be\/,soundcloud\.com\/,bandcamp\.com\/track\/ + ## In-game features ## Toggle for having jobs load up from the jobconfig.toml (or jobs.txt) file. Will use codebase defaults if commented out. diff --git a/tgstation.dme b/tgstation.dme index 93808249bfd..45b8e986f67 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -2354,6 +2354,7 @@ #include "code\modules\admin\verbs\possess.dm" #include "code\modules\admin\verbs\pray.dm" #include "code\modules\admin\verbs\reestablish_db_connection.dm" +#include "code\modules\admin\verbs\request_internet_sound.dm" #include "code\modules\admin\verbs\requests.dm" #include "code\modules\admin\verbs\secrets.dm" #include "code\modules\admin\verbs\selectequipment.dm" diff --git a/tgui/packages/tgui-panel/audio/NowPlayingWidget.js b/tgui/packages/tgui-panel/audio/NowPlayingWidget.js index aeef414ec00..4495c2a8a1f 100644 --- a/tgui/packages/tgui-panel/audio/NowPlayingWidget.js +++ b/tgui/packages/tgui-panel/audio/NowPlayingWidget.js @@ -6,7 +6,7 @@ import { toFixed } from 'common/math'; import { useDispatch, useSelector } from 'common/redux'; -import { Button, Collapsible, Flex, Knob } from 'tgui/components'; +import { Button, Collapsible, Flex, Knob, Section } from 'tgui/components'; import { useSettings } from '../settings'; import { selectAudio } from './selectors'; @@ -17,7 +17,7 @@ export const NowPlayingWidget = (props, context) => { title = audio.meta?.title, URL = audio.meta?.link, Artist = audio.meta?.artist || 'Unknown Artist', - upload_date = audio.meta?.upload_date || 'Date Unknown', + upload_date = audio.meta?.upload_date || 'Unknown Date', album = audio.meta?.album || 'Unknown Album', duration = audio.meta?.duration, date = !isNaN(upload_date) @@ -41,13 +41,33 @@ export const NowPlayingWidget = (props, context) => { }}> { - - Url: {URL}
- Duration: {duration}
- Artist: {Artist}
- Album: {album}
- Uploaded: {date} -
+
+ {URL !== 'Song Link Hidden' && ( + + Url: {URL} + + )} + + Duration: {duration} + + {Artist !== 'Song Artist Hidden' && + Artist !== 'Unknown Artist' && ( + + Artist: {Artist} + + )} + {album !== 'Song Album Hidden' && album !== 'Unknown Album' && ( + + Album: {album} + + )} + {upload_date !== 'Song Upload Date Hidden' && + upload_date !== 'Unknown Date' && ( + + Uploaded: {date} + + )} +
} diff --git a/tgui/packages/tgui/interfaces/RequestManager.js b/tgui/packages/tgui/interfaces/RequestManager.js index ecc2a0ad515..d1ec5370f8a 100644 --- a/tgui/packages/tgui/interfaces/RequestManager.js +++ b/tgui/packages/tgui/interfaces/RequestManager.js @@ -85,6 +85,7 @@ const displayTypeMap = { 'request_syndicate': 'SYNDICATE', 'request_nuke': 'NUKE CODE', 'request_fax': 'FAX', + 'request_internet_sound': 'INTERNET SOUND', }; const RequestType = (props) => { @@ -121,6 +122,9 @@ const RequestControls = (props, context) => { {request.req_type === 'request_fax' && ( )} + {request.req_type === 'request_internet_sound' && ( + + )} ); }; diff --git a/tgui/packages/tgui/styles/interfaces/RequestManager.scss b/tgui/packages/tgui/styles/interfaces/RequestManager.scss index f560918ed3d..1003440243a 100644 --- a/tgui/packages/tgui/styles/interfaces/RequestManager.scss +++ b/tgui/packages/tgui/styles/interfaces/RequestManager.scss @@ -12,6 +12,7 @@ $color-centcom: colors.bg(colors.$yellow) !default; $color-syndicate: colors.bg(colors.$red) !default; $color-nuke: colors.bg(colors.$yellow) !default; $color-fax: colors.bg(colors.$green) !default; +$color-music: colors.bg(colors.$teal) !default; $color-muted: colors.bg(colors.$label) !default; $color-header: colors.bg(colors.$label) !default; $background-color: base.$color-bg-section !default; @@ -44,6 +45,10 @@ $text-color: base.$color-fg !default; color: $color-fax; } +.RequestManager__request_internet_sound { + color: $color-music; +} + .RequestManager__header { line-height: 1.375rem; min-height: 1.375rem;