diff --git a/amplipi/ctrl.py b/amplipi/ctrl.py index e55f67b67..84ae5f7d0 100644 --- a/amplipi/ctrl.py +++ b/amplipi/ctrl.py @@ -204,15 +204,15 @@ def reinit(self, settings: models.AppSettings = models.AppSettings(), change_not self.status = models.Status.parse_obj(default_config) self.save() - # make sure the config file contains the auxoptical stream + # make sure the config file contains the aux stream has_stream = False for s in self.status.streams: - if s.type == "auxoptical": + if s.type == "aux": has_stream = True break if not has_stream: - self.create_stream(models.Stream(type="auxoptical", name="Aux/Optical", optical=False)) + self.create_stream(models.Stream(type="aux", name="Aux")) # populate system info self._online_cache = utils.TimeBasedCache(self._check_is_online, 5, 'online') diff --git a/amplipi/models.py b/amplipi/models.py index 435efa250..6a50d17b8 100644 --- a/amplipi/models.py +++ b/amplipi/models.py @@ -465,7 +465,7 @@ class Stream(Base): * internetradio * spotify * plexamp - * auxoptical + * aux * file * fmradio * lms @@ -485,7 +485,6 @@ class Stream(Base): index: Optional[int] = Field(description='RCA index') disabled: Optional[bool] = Field(description="Soft disable use of this stream. It won't be shown as a selectable option") ap2: Optional[bool] = Field(description='Is Airplay stream AirPlay2?') - optical: Optional[bool] = Field(description='Is Aux/Optical stream optical?') # add examples for each type of stream class Config: @@ -651,7 +650,6 @@ class StreamUpdate(BaseUpdate): server: Optional[str] ap2: Optional[bool] = Field(description='Is Airplay stream AirPlay2?') disabled: Optional[bool] = Field(description="Soft disable use of this stream. It won't be shown as a selectable option") - optical: Optional[bool] = Field(description='Is AuxOptical stream optical?') class Config: schema_extra = { diff --git a/amplipi/streams.py b/amplipi/streams.py index 721b5f868..6bcb09a00 100644 --- a/amplipi/streams.py +++ b/amplipi/streams.py @@ -973,11 +973,10 @@ def info(self) -> models.SourceInfo: source.track = "Not currently supported" return source -class AuxOptical(BaseStream): - """ A stream to play from the aux/optical input. """ - def __init__(self, name: str, optical: bool, disabled: bool = False, mock: bool = False): - super().__init__('aux/optical', name, disabled=disabled, mock=mock) - self.optical = optical +class Aux(BaseStream): + """ A stream to play from the aux input. """ + def __init__(self, name: str, disabled: bool = False, mock: bool = False): + super().__init__('aux', name, disabled=disabled, mock=mock) self.bkg_thread = None def reconfig(self, **kwargs): @@ -986,9 +985,6 @@ def reconfig(self, **kwargs): self.disabled = kwargs['disabled'] if 'name' in kwargs: self.name = kwargs['name'] - if 'optical' in kwargs: - self.optical = kwargs['optical'] - reconnect_needed = True if reconnect_needed: last_src = self.src self.disconnect() @@ -1007,9 +1003,7 @@ def connect(self, src): return # Set input source - print(f'setting input source to {"optical" if self.optical else "aux"}...') - - amixer_command = f'amixer -D usb71 set "PCM Capture Source" "{"IEC958 In" if self.optical else "Line"}"' + amixer_command = f'amixer -D usb71 set "PCM Capture Source" "Line"' subprocess.check_call(amixer_command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) @@ -1041,9 +1035,9 @@ def disconnect(self): def info(self) -> models.SourceInfo: source = models.SourceInfo(name=self.full_name(), - track="Optical" if self.optical else "Aux", + track="Aux", state=self.state, - img_url=f'static/imgs/{"optical" if self.optical else "aux"}_input.png') + img_url=f'static/imgs/aux_input.png') return source class FilePlayer(BaseStream): @@ -1384,7 +1378,7 @@ def send_cmd(self, cmd): # Simple handling of stream types before we have a type heirarchy AnyStream = Union[RCA, AirPlay, Spotify, InternetRadio, DLNA, Pandora, Plexamp, - AuxOptical, FilePlayer, FMRadio, LMS, Bluetooth] + Aux, FilePlayer, FMRadio, LMS, Bluetooth] def build_stream(stream: models.Stream, mock=False) -> AnyStream: """ Build a stream from the generic arguments given in stream, discriminated by stream.type @@ -1409,8 +1403,8 @@ def build_stream(stream: models.Stream, mock=False) -> AnyStream: return InternetRadio(name, args['url'], args.get('logo'), disabled=disabled, mock=mock) if stream.type == 'plexamp': return Plexamp(name, args['client_id'], args['token'], disabled=disabled, mock=mock) - if stream.type == 'auxoptical': - return AuxOptical(name, args['optical'], disabled=disabled, mock=mock) + if stream.type == 'aux': + return Aux(name, disabled=disabled, mock=mock) if stream.type == 'fileplayer': return FilePlayer(name, args['url'], disabled=disabled, mock=mock) if stream.type == 'fmradio': diff --git a/docs/QUICK_START.md b/docs/QUICK_START.md index abd300a1f..d053bdb9b 100644 --- a/docs/QUICK_START.md +++ b/docs/QUICK_START.md @@ -15,7 +15,6 @@ All of the connections will be made to the back panel. Here's a quick reference - **CONTROLLER**: Connections to the embedded Raspberry Pi Controller - **SERVICE**: USB mini connection for re-imaging the Pi's EMMC - **USB**: USB A ports for connecting peripherals such as additional storage devices - - **OPTICAL IN**: SPDIF audio input, planned to be used for extra inputs - **AUX IN**: Additional stereo input, planned to be used for announcements - **HDMI OUT**: The Pi's HMDI output, can be used for visualizations or development - **ETHERNET**: Network connection, see [Networking](#networking) diff --git a/pyproject.toml b/pyproject.toml index 9e5f76d4b..53d76f898 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "amplipi" -version = "0.3.1" +version = "0.3.0+4cf6490-aux-optical-stream-dirty" description = "A Pi-based whole house audio controller" authors = [ "Lincoln Lorenz ", diff --git a/web/src/pages/Settings/Streams/StreamTemplates.json b/web/src/pages/Settings/Streams/StreamTemplates.json index fefe33922..d363830a6 100644 --- a/web/src/pages/Settings/Streams/StreamTemplates.json +++ b/web/src/pages/Settings/Streams/StreamTemplates.json @@ -111,6 +111,12 @@ { "name": "Spotify Device", "type": "spotify", + "noCreate": true, + "fields": [] + }, + { + "name": "Aux Port", + "type": "aux", "fields": [] }, { diff --git a/web/src/utils/getIcon.jsx b/web/src/utils/getIcon.jsx index efe649adb..e6c0cf94a 100644 --- a/web/src/utils/getIcon.jsx +++ b/web/src/utils/getIcon.jsx @@ -8,6 +8,7 @@ import plexamp from "@/../static/imgs/plexamp.png"; import lms from "@/../static/imgs/lms.png"; import internetradio from "@/../static/imgs/internet_radio.png"; import rca from "@/../static/imgs/rca_inputs.jpg"; +import aux from "@/../static/imgs/aux_input.png"; export const getIcon = (type) => { if (type === null || type === undefined) { @@ -44,6 +45,9 @@ export const getIcon = (type) => { case "INTERNET RADIO": return internetradio; + case "AUX": + return aux; + default: return internetradio; } diff --git a/web/static/imgs/optical_input.png b/web/static/imgs/optical_input.png deleted file mode 100644 index 8fb238577..000000000 Binary files a/web/static/imgs/optical_input.png and /dev/null differ diff --git a/web/static/imgs/optical_input.svg b/web/static/imgs/optical_input.svg deleted file mode 100644 index 068b85586..000000000 --- a/web/static/imgs/optical_input.svg +++ /dev/null @@ -1,112 +0,0 @@ - - - -image/svg+xml diff --git a/web/static/js/settings.js b/web/static/js/settings.js deleted file mode 100644 index 500975f5e..000000000 --- a/web/static/js/settings.js +++ /dev/null @@ -1,869 +0,0 @@ -/* Set up the necessary arrays */ -var streams = []; -var zones = []; -var groups = []; -var radiobrowser_base_url = ''; - -class Stream { - constructor(name, description) { - this.name = name; - this.description = description; - } -} -const STREAM_TYPES_ = { - auxoptical: new Stream("auxoptical", "Aux/Optical"), - airplay: new Stream("airplay", "AirPlay Device"), - dlna: new Stream("dlna", "DLNA"), - fmradio: new Stream("fmradio", "FM Radio Station"), - internetradio: new Stream("internetradio", "Internet Radio Station"), - lms: new Stream("lms", "LMS client"), - pandora: new Stream("pandora", "Pandora Station"), - plexamp: new Stream("plexamp", "Plexamp"), - spotify: new Stream("spotify", "Spotify Device"), - bluetooth: new Stream("blueooth", "Bluetooth Device"), - rca: new Stream("rca", "RCA Input"), -}; - -/* updateSettings clears out the previous API information and shows the current state */ -function updateSettings() { - $("#settings-tab-inputs-stream-title").text("Select a stream"); - $("#settings-tab-inputs-stream-selection").empty(); - $("#settings-tab-inputs-config").html(""); - $("#settings-tab-zones-title").text("Select a zone"); - $("#settings-tab-zones-selection").empty(); - $("#settings-tab-zones-config").html(""); - $("#settings-tab-groups-title").text("Select a group"); - $("#settings-tab-groups-selection").empty(); - $("#settings-tab-groups-config").html(""); - $.get("/api", function(data) { - /* Remove sources for now, TODO: source configuration needs its own settings page - $.each(data.sources, function(k, v) { - streams[v.id] = v; - $("#settings-tab-inputs-stream-selection").append( - '
  • ' + - v.name + - ' ' + `Source ${v.id+1}` + '' - ); - }); - */ - $.each(data.streams, function(k, v) { - streams[v.id] = v; - $("#settings-tab-inputs-stream-selection").append( - '
  • ' + - v.name + - ' ' + v.type + '' - ); - }); - $.each(data.zones, function(k, v) { - zones[v.id] = v; - $("#settings-tab-zones-selection").append( - '
  • ' + - v.name + - ' ' + (v.disabled ? 'disabled' : '') + '' - ); - }); - $.each(data.groups, function(k, v) { - groups[v.id] = v; - $("#settings-tab-groups-selection").append( - '
  • ' + - v.name - ); - }); - }); -}; - - -function cache_internetradio_server(reset_cache=false) { - /* Try to Get a random radio-browser server */ - if (!radiobrowser_base_url || reset_cache) { - fetch('http://all.api.radio-browser.info/json/servers') - .then(response => response.json()) - .then(hosts => radiobrowser_base_url = "https://" + hosts[Math.floor(Math.random() * hosts.length)].name); - } -} -/* On page load, get list of current streams */ -$(function() { - - /* Disable enter key from submitting form when on internet radio search */ - $(window).keydown(function(event){ - if(event.keyCode == 13) { - if (event.target.id == 'internetradio_search_name_txt') { // Intercept enter key for internet radio search - $("#internetradio_search_name_btn").trigger('click'); - event.preventDefault(); - return false; - } - } - }); - - /* Show new stream options */ - $("#settings-tab-inputs-new-stream").click(function(){ - $("#settings-tab-inputs-stream-selection li").removeClass('active'); // De-select "active" stream on the left menu if had been selected - $(this).addClass('active'); - $("#settings-tab-inputs-stream-title").text("Add a new stream to AmpliPi"); - let options = ''; - for (stream in STREAM_TYPES_) { - options += ''; - } - var html = ` -
    -
    - - -
    -
    -
    - `; - $("#settings-tab-inputs-config").html(html); - }); - - function show_editable_stream_settings() { - $('#settings-tab-inputs-stream-selection li').removeClass('active'); - $("#settings-tab-inputs-new-stream").removeClass('active'); - $(this).addClass('active'); - var s = streams[$(this).data("id")]; - var stream_type = s.type ? STREAM_TYPES_[s.type] : `source ${s.id+1}`; - - $("#settings-tab-inputs-stream-title").text(s.name + " (" + stream_type.name + ")"); - var html = ` - -
    - `; - - if (stream_type == STREAM_TYPES_.fmradio) { - html += ` -
    - An extra USB dongle is needed to support FM Radio see RTL SDR -
    - `; - } - - html += ` -
    - - - This name can be anything - it will be used to select this stream from the source selection dropdown -
    `; - - disable_html = ` -
    - - - - Don't show this stream in the input dropdown -
    `; - - if (s.type) { - // sources can't be disabled yet - html += disable_html; - } - - switch (stream_type) { - case STREAM_TYPES_.airplay: - html += ` -
    - - - - Make this stream Airplay2; only one Airplay2 stream can be running at a time. -
    - `; - break; - case STREAM_TYPES_.auxoptical: - html += ` -
    - - - - Check to make this Stream get input from optical in instead of aux. Only one can be used at a time. -
    - `; - break; - case STREAM_TYPES_.fmradio: - html += ` -
    - - - Enter an FM frequency 87.5 to 107.9. Requires an RTL-SDR compatible USB dongle. -
    -
    - - - Default built-in logo is: static/imgs/fmradio.png -
    - `; - break; - case STREAM_TYPES_.internetradio: - html += ` -
    - - - Audio URL must be supported by VLC. -
    -
    - - -
    - `; - break; - case STREAM_TYPES_.lms: - html += ` -
    - - - Optional LMS server hostname (without port). Example: mylmsserver -
    - `; - break; - case STREAM_TYPES_.pandora: - html += ` -
    - - -
    -
    - - -
    -
    - - - Station ID is the numeric section of a Pandora station link. Example: ID = 4610303469018478727 from https://www.pandora.com/station/play/4610303469018478727 -
    - `; - break; - case STREAM_TYPES_.plexamp: - html += ` -
    - Click the Request Authentication button to open a Plex authorization page. Signing into Plex will generate a UUID and Token, shown below -
    -
    - - -
    -
    - - -
    -
    - - -
    - `; - break; - case STREAM_TYPES_.bluetooth: - // TODO: there can be exactly zero or one bluetooth streams. how do we limit the stream count? also, should they even be nameable? what purpose is a name other than "bluetooth" - html += ` - - ` - break; - } - - // Analog RCA input, can't be deleted. TODO: make RCA inputs disable-able - hide_del = (s.type == null) || (stream_type == STREAM_TYPES_.rca) ? 'style="display:none"' : ''; - html += ` - - - -
    - `; - $("#settings-tab-inputs-config").html(html); - } - - $("#settings-tab-inputs-stream-selection").on("click", ".stream", show_editable_stream_settings); - $("#settings-tab-inputs-stream-selection").on("change", ".stream", show_editable_stream_settings); - - /* Show new stream settings */ - $("#settings-tab-inputs-config").on("change", "#new_stream_type", function() { - var name_html = ` -
    - - - This name can be anything - it will be used to select this stream from the source selection dropdown -
    `; - var stream_type = ''; - try { - stream_type = STREAM_TYPES_[$(this).val()]; - } catch (e) { - // Ignore TypeErrors which occur with the dummy "select a stream" option. - if (!(e instanceof TypeError)) { - throw e; - } - } - var html = name_html; - switch (stream_type) { - case STREAM_TYPES_.airplay: - html += ` -
    - - - - Make this stream Airplay2; only one Airplay2 stream can be running at a time. -
    - `; - break; - case STREAM_TYPES_.fmradio: - html = ` -
    - An extra USB dongle is needed to support FM Radio see RTL SDR -
    ` + name_html + ` -
    - - - Enter an FM frequency 87.5 to 107.9. Requires an RTL-SDR compatible USB dongle. -
    -
    - - - Default built-in logo is: static/imgs/fmradio.png -
    `; - break; - case STREAM_TYPES_.internetradio: - html = ` -
    - - - Optional. Searches radio-browser for internet radio stations. -
    -
    - - -
    -
    -
    ` + name_html + ` -
    - - - Audio URL must be supported by VLC. -
    -
    - - - Optionally provide an image URL for the station. -
    `; - break; - - case STREAM_TYPES_.lms: - html += ` -
    - - - Optional LMS server hostname (without port). Example: mylmsserver -
    - `; - break; - case STREAM_TYPES_.pandora: - html += ` -
    - - -
    -
    - - -
    -
    - - - Station ID is the numeric section of a Pandora station link. Example: ID = 4610303469018478727 from https://www.pandora.com/station/play/4610303469018478727 -
    `; - break; - case STREAM_TYPES_.plexamp: - html += ` -
    - Click the Request Authentication button to open a Plex authorization page. Signing into Plex will generate a UUID and Token, shown below -
    -
    - - -
    -
    - - -
    -
    - - -
    `; - break; - - } - html += ` - - - `; - $("#new_stream_settings").html(html); - - cache_internetradio_server(); - }); - - /* Search for internet radio stations */ - $(document).on('click', '#internetradio_search_name_btn', function () { - console.log('Searching for station by name: ' + $("#internetradio_search_name_txt").val()); - search_indicator = $(".internet-radio-search-button i"); - search_indicator.toggleClass('fa-circle-notch', true); - search_indicator.toggleClass('fa-exclamation-triangle', false); - const keywords = $("#internetradio_search_name_txt").val(); - console.log("Using radio-browser server ", radiobrowser_base_url); - $.ajax({ - type: "GET", - url: `${radiobrowser_base_url}/json/stations/byname/${keywords}?limit=100`, - contentType: "application/json", - timeout: 2500, - success: function(data) { - search_indicator.toggleClass('fa-circle-notch', false); - search_indicator.toggleClass('fa-exclamation-triangle', false); - $("#internetradio_search_name_results").html("

    Search Results

    "); - var details = ''; - var numResults = 0; - $.each(data, function(index, value) { - ++numResults; - if (value.bitrate && value.codec) { details = '(' + value.bitrate + 'kbps ' + value.codec + ')'; } - $('#internetradio_search_name_results').append( - '
    ' + - value.name + - ' ' + - details + - 'More Info' + - 'Use
    ' - ); - }); - if (!numResults) { $('#internetradio_search_name_results').append("No stations found."); } - }, - error: function () { - // set the indicator to failed - search_indicator.toggleClass('fa-circle-notch', false); - search_indicator.toggleClass('fa-exclamation-triangle', true); - $('#internetradio_search_name_results').html("

    Search Results

    Error searching for stations. Check your internet access and try again."); - // try to get a new internet radio server, do this at the end since it can take several seconds - cache_internetradio_server(true); - } - }); - }); - - /* Add an internet radio station from search screen */ - $(document).on('click', '.addStation', function () { - var name = $(this).data('stationname'); - var url = $(this).data('stationurl'); - var logo = $(this).data('stationlogo'); - $('#new_stream_name').val(name); - $('#new_internetradio_url').val(url); - $('#new_internetradio_logo').val(logo); - return false; - }); - - /* Add New Stream and Reload Page */ - $("#settings-tab-inputs-config").on("submit", "#settings-tab-inputs-new-stream-form", function(e) { - e.preventDefault(); // avoid to execute the actual submit of the form. - - var $form = $("#settings-tab-inputs-new-stream-form"); - var validation = checkFormData($("#settings-tab-inputs-new-stream-form :input")) - if (!validation) { return; } - - var formData = getFormData($form); - - $.ajax({ - type: "POST", - url: '/api/stream', - data: JSON.stringify(formData), - contentType: "application/json", - success: updateSettings - }); - }); - - - /* Save stream changes and reload page */ - $("#settings-tab-inputs-config").on("submit", "#editStreamForm", function(e) { - e.preventDefault(); // avoid to execute the actual submit of the form. - - var $form = $("#editStreamForm"); - var validation = checkFormData($("#editStreamForm :input")); - if (!validation) { return; } - - var formData = getFormData($form); - var s = streams[document.getElementById('edit-sid').value]; - console.log(s) - if (s.type == null) { - var nurl = '/api/sources/' - } else { - var nurl = '/api/streams/' - } - - $.ajax({ - type: "PATCH", - url: nurl + $("#edit-sid").val(), - data: JSON.stringify(formData), - contentType: "application/json", - success: updateSettings - }); - }); - - - /* Delete stream and reload stream list and settings */ - $("#settings-tab-inputs-config").on("click", "#delete", function() { - $.ajax({ - url: '/api/streams/' + document.getElementById("edit-sid").value, - type: 'DELETE', - success: updateSettings - }); - }); - - /* Show selected zone settings */ - $("#settings-tab-zones-selection").on("click", ".zone-config", function() { - $('#settings-tab-zones-selection li').removeClass('active'); - $(this).addClass('active'); - var z = zones[$(this).data("id")]; - console.log(z) - - $("#settings-tab-zones-title").text(z.name); - - /* TODO: min and max volumes should be taken from models.py, can we add this to the API? */ - /* The checkbox here use a hidden input to send the value of false by default. - * The actual checkbox will either send nothing, or send true which overrides the false. */ - var html = ` - -
    -
    - - -
    -
    - - - -80 to 0 dB, default -80. Must be at least 20 dB lower than max volume. -
    -
    - - - -80 to 0 dB, default 0. Must be at least 20 dB higher than min volume. -
    -
    - - - - Disabling a zone removes its mute and volume controls. A zone should be disabled if it isn't going to be used, or has no speakers connected to it -
    - `; - - html += ` - - -
    - `; - $("#settings-tab-zones-config").html(html); - }); - - /* Save zone changes and reload page */ - $("#settings-tab-zones-config").on("submit", "#editZoneForm", function(e) { - e.preventDefault(); // avoid to execute the actual submit of the form. - - var $form = $("#editZoneForm"); - var validation = checkFormData($("#editZoneForm :input")); - if (!validation) { return; } - - var formData = getFormData($form); - var z = zones[document.getElementById('edit-zid').value]; - console.log(z) - - $.ajax({ - type: "PATCH", - url: '/api/zones/' + $("#edit-zid").val(), - data: JSON.stringify(formData), - contentType: "application/json", - success: updateSettings - }); - }); - - /* Show new group options */ - $("#settings-tab-groups-new-group").click(function(){ - $("#settings-tab-groups-selection li").removeClass('active'); // De-select "active" group on the left menu if had been selected - $(this).addClass('active'); - $("#settings-tab-groups-title").text("Add a new group to AmpliPi"); - var zone_html = ``; - - for (const zone in zones) { - zone_html += ` - - `; - }; - - var html = ` -
    -
    - - -
    -
    - -
    - - -
    - `; - $("#settings-tab-groups-config").html(html); - /* Initialize selectpicker */ - if (/Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(navigator.userAgent)) { - $('#zone-picker').selectpicker('mobile'); - } else { - $('#zone-picker').selectpicker({}); - } - }); - - /* Show selected group settings */ - $("#settings-tab-groups-selection").on("click", ".group-config", function() { - $('#settings-tab-groups-selection li').removeClass('active'); - $("#settings-tab-groups-new-group").removeClass('active'); - $(this).addClass('active'); - var g = groups[$(this).data("id")]; - console.log(g) - $("#settings-tab-groups-title").text(g.name); - var zone_html = ``; - - for (const zone in zones) { - const incl = g.zones.includes(parseInt(zone)); - zone_html += ` - - `; - }; - - var html = ` - -
    -
    - - -
    -
    - -
    - - - -
    - `; - $("#settings-tab-groups-config").html(html); - /* Initialize selectpicker */ - if (/Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(navigator.userAgent)) { - $('#zone-picker').selectpicker('mobile'); - } else { - $('#zone-picker').selectpicker({}); - } - }); - - /* Add New Group and Reload Page */ - $("#settings-tab-groups-config").on("submit", "#settings-tab-groups-new-group-form", function(e) { - e.preventDefault(); // avoid to execute the actual submit of the form. - - var $form = $("#settings-tab-groups-new-group-form"); - var validation = checkFormData($("#settings-tab-groups-new-group-form :input")) - console.log(validation); - if (!validation) { return; } - - var formData = getGroupData($form); - console.log(formData); - - $.ajax({ - type: "POST", - url: '/api/group', - data: JSON.stringify(formData), - contentType: "application/json", - success: updateSettings - }); - }); - - - /* Save group changes and reload page */ - $("#settings-tab-groups-config").on("submit", "#editGroupForm", function(e) { - e.preventDefault(); // avoid to execute the actual submit of the form. - - var $form = $("#editGroupForm"); - var validation = checkFormData($("#editGroupForm :input")); - if (!validation) { return; } - - var formData = getGroupData($form); - var g = groups[document.getElementById('edit-gid').value]; - console.log(g) - - $.ajax({ - type: "PATCH", - url: '/api/groups/' + $("#edit-gid").val(), - data: JSON.stringify(formData), - contentType: "application/json", - success: updateSettings - }); - }); - - - /* Delete stream and reload stream list and settings */ - $("#settings-tab-groups-config").on("click", "#delete", function() { - $.ajax({ - url: '/api/groups/' + document.getElementById("edit-gid").value, - type: 'DELETE', - success: updateSettings - }); - }); - - /* Make sure all required field are filled */ - function checkFormData($form) { - var isValid = true; - var sub_text = document.getElementById('submitHelp'); - sub_text.textContent = ""; - $form.each(function() { - if ($(this).data("required") == true && $(this).val() === '' && $(this).is(':visible')) { - isValid = false; - sub_text.textContent = "Please fill out all required fields."; - $(this).addClass('is-invalid'); - } - }); - return isValid; - } - - /* Return form data in AmpliPi's JSON format */ - function getFormData($form) { - var unindexed_array = $form.serializeArray(); - var indexed_array = {}; - - $.map(unindexed_array, function(n, i){ - indexed_array[n['name']] = n['value']; - }); - - return indexed_array; - } - - /* Unique form parsing for Groups */ - function getGroupData($form) { - var unindexed_array = $form.serializeArray(); - var indexed_array = {}; - var zone_array = []; - const zone_sel = document.getElementById('zone-picker'); - for (const option of document.querySelectorAll('#zone-picker option')) { - const val = Number.parseInt(option.value); - if (option.selected) { - zone_array.push(val); - } - } - - $.map(unindexed_array, function(n, i){ - indexed_array[n['name']] = n['value']; - indexed_array['zones'] = zone_array; - }); - - return indexed_array; - } -}); - -/* Plexamp Authentication Functions */ -async function plex_pin_req() { - // Request a Plex pin to use for interacting with the Plex API - document.getElementById('plexamp-connect').textContent = "Sending request..."; - let myuuid = uuidv4(); // UUID used as the 'clientIdentifier' for Plexamp requests/devices - let details = { } - let response = await fetch('https://plex.tv/api/v2/pins', { - method: 'POST', - headers: { - 'accept': 'application/json', - 'Content-Type': 'application/x-www-form-urlencoded' - }, - body: "strong=true&X-Plex-Product=AmpliPi&X-Plex-Client-Identifier=" + myuuid - }); // The actual pin request is sent. response holds the pin, pin code, and our UUID. - response.json() - .then(function(response){ // Make URL for the Plex Account Login - const purl = `https://app.plex.tv/auth#?clientID=${response.clientIdentifier}&code=${response.code}&context%5Bdevice%5D%5Bproduct%5D=AmpliPi`; - details.id = response.id; // The actual PIN - details.code = response.code; // A code associated with the PIN - details.uuid = response.clientIdentifier; // Our UUID associated with the PIN and authToken - details.authToken = null; // Will eventually hold a token from plex_token_ret on a successful sign-in - console.log(details); - window.open(purl, "_blank"); // Open 'purl' in a new tab in the current browser window - }); - return details; // Pin, code, UUID, and authToken are used in the other functions -} - -async function plex_token_ret(details) { - // Attempt to retrieve the plex token (this will return 'null' until the user enters their Plex account details) - // NOTE: this token will only work for plexamp if the user has a Plex Pass subscription - document.getElementById('plexamp-connect').textContent = "Awaiting Plex sign-in..."; - let response = await fetch('https://plex.tv/api/v2/pins/'+details.id, { - method: 'GET', - headers: { - 'accept': 'application/json', - 'Content-Type': 'application/x-www-form-urlencoded', - 'code': details.code, - 'X-Plex-Client-Identifier': details.uuid - }, - }); // Information related to our PIN was requested. Parse that info to see if we've authenticated yet - response.json().then(function(response){ - console.log("Token: " + response.authToken); - details.authToken = response.authToken; - console.log("Time remaining: " + response.expiresIn); - details.expiresIn = response.expiresIn; - }); - return details; -} - -async function plex_stream(details) { - // Create Plexamp stream using AmpliPi's API - var req = { - "name": details.name, - "client_id": details.uuid, - "token": details.authToken, - "type": "plexamp" - } // POST a new stream to the AmpliPi API using the newly authenticated credentials - sendRequest('/stream', 'POST', req); - console.log(`Creating stream with these parameters: name = ${req.name}, UUID = ${req.client_id}, and token = ${req.token}`); -} - -function sleepjs(ms) { - return new Promise(resolve => setTimeout(resolve, ms)); // JavaScript sleep function -} - -async function plexamp_create_stream() { - // Connect to Plex's API and add a Plexamp stream to AmpliPi - var connect_button = document.getElementById('plexamp-connect'); - var reset_button = document.getElementById('plexamp-reset'); - var msg_box1 = document.getElementById('plexHelp'); - var msg_box2 = document.getElementById('submitHelp'); - var uuid_text = document.getElementById('client_id'); - var auth_text = document.getElementById('token'); - connect_button.disabled = true; - let details = await plex_pin_req(); // Request a pin - await sleepjs(2000); // Wait for info to propagate over - reset_button.style.display = "inline-block"; - msg_box1.style.display = "none"; - - do { - let details2 = await plex_token_ret(details); // Retrieve our token - await sleepjs(2000); // poll the plex servers slowly - if (details2.expiresIn == null){ - msg_box1.textContent = "Timed out while waiting for response from Plex"; - msg_box1.style.color = "yellow"; - msg_box1.style.display = "block"; - msg_box1.style.alignSelf = "left"; - break; // Break when you run out of time (30 minutes, set by Plex) - } - details = details2; // Update authToken state and time until expiration - } while (details.authToken == null); // "== null" should also check for undefined - if (details.authToken){ - connect_button.style.display = "none"; - reset_button.style.display = "none"; - auth_text.value = `${details.authToken}`; - uuid_text.value = `${details.uuid}`; - msg_box2.textContent = `Client_id and Token successfully generated!`; - // plex_stream(details); // Create a Plexamp stream using the API! - } -}