├── .gitignore ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── feature_request.md │ └── bug-or-issue.md ├── workflows │ └── IssueResponse.yml └── label-commenter-config.yml ├── hacs.json ├── Plex_Assistant_DialogFlow.zip ├── .deepsource.toml ├── custom_components └── plex_assistant │ ├── services.yaml │ ├── const.py │ ├── manifest.json │ ├── intent.py │ ├── plex_assistant.py │ ├── translations │ ├── en.json │ ├── hu.json │ ├── it.json │ └── es.json │ ├── config_flow.py │ ├── __init__.py │ ├── process_speech.py │ ├── helpers.py │ └── localize.py ├── info.md ├── troubleshooting.md ├── LICENSE ├── ver_one_update.md ├── translation.md ├── OLD_README.md └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /.vscode 2 | /.dccache -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [maykar] 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | -------------------------------------------------------------------------------- /hacs.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Plex Assistant", 3 | "homeassistant": "2021.2.0" 4 | } 5 | -------------------------------------------------------------------------------- /Plex_Assistant_DialogFlow.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maykar/plex_assistant/HEAD/Plex_Assistant_DialogFlow.zip -------------------------------------------------------------------------------- /.deepsource.toml: -------------------------------------------------------------------------------- 1 | version = 1 2 | 3 | [[analyzers]] 4 | name = "python" 5 | enabled = true 6 | 7 | [analyzers.meta] 8 | runtime_version = "3.x.x" 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | -------------------------------------------------------------------------------- /custom_components/plex_assistant/services.yaml: -------------------------------------------------------------------------------- 1 | command: 2 | description: Send command to Plex 3 | fields: 4 | command: 5 | description: '[Required] Command to send' 6 | example: Play Evil Dead on the Living Room TV 7 | -------------------------------------------------------------------------------- /custom_components/plex_assistant/const.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from homeassistant.const import __version__ 4 | from awesomeversion import AwesomeVersion 5 | 6 | DOMAIN = "plex_assistant" 7 | HA_VER_SUPPORTED = AwesomeVersion(__version__) >= AwesomeVersion("2021.2.0") 8 | _LOGGER = logging.getLogger(__name__) 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-or-issue.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug or Issue 3 | about: 'Read before submitting: https://github.com/maykar/plex_assistant/blob/master/troubleshooting.md' 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | 14 | -------------------------------------------------------------------------------- /custom_components/plex_assistant/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "plex_assistant", 3 | "name": "Plex Assistant", 4 | "version": "1.1.9", 5 | "documentation": "https://github.com/maykar/plex_assistant", 6 | "dependencies": ["media_player"], 7 | "config_flow": true, 8 | "codeowners": ["@maykar"], 9 | "requirements": [ 10 | "gTTs>=2.2.1", 11 | "pychromecast>=8.0.0", 12 | "rapidfuzz==1.1.1", 13 | "plexapi>=4.3.0", 14 | "awesomeversion>=21.2.2" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /.github/workflows/IssueResponse.yml: -------------------------------------------------------------------------------- 1 | on: 2 | issues: 3 | types: 4 | - labeled 5 | - unlabeled 6 | pull_request: 7 | types: 8 | - labeled 9 | - unlabeled 10 | 11 | jobs: 12 | comment: 13 | runs-on: ubuntu-18.04 14 | steps: 15 | - uses: actions/checkout@v2 16 | with: 17 | ref: master 18 | 19 | - name: Label Commenter 20 | uses: peaceiris/actions-label-commenter@v1 21 | with: 22 | github_token: ${{ secrets.GITHUB_TOKEN }} 23 | -------------------------------------------------------------------------------- /info.md: -------------------------------------------------------------------------------- 1 | # ❱ Plex Assistant 2 | 3 | Plex Assistant is a Home Assistant integration for casting Plex media to Google devices, Sonos devices, and Plex clients with Google Assistant, HA's conversation integration, and more. 4 | 5 | Example: `"Hey Google, tell Plex to play The Walking Dead on the Downstairs TV."` 6 | 7 | **[Visit the readme for more info, config, setup, supported languages, and available commands.](https://github.com/maykar/plex_assistant)** 8 | 9 | #### Support Development 10 | - :coffee:  [Buy me a coffee](https://www.buymeacoffee.com/FgwNR2l) 11 | - :heart:  [Sponsor me on GitHub](https://github.com/sponsors/maykar) 12 | - :keyboard:  Help with development, documentation, or [translation](https://github.com/maykar/plex_assistant/blob/master/translation.md) 13 | -------------------------------------------------------------------------------- /troubleshooting.md: -------------------------------------------------------------------------------- 1 | ## Troubleshooting Plex Assistant 2 | 3 | Whenever posting an issue include as much of the following info as you can: 4 | 5 | * Any errors related to Plex Assistant or Plex in your logs along with debug info (see below on how to enable debug) 6 | * The trigger method you're using (IFTTT, DialogFlow, or HA Conversation) 7 | * The method used to install (HACS or manually) 8 | * If HA's Plex Integration works without issue 9 | * The command you are using (if the issue is command specific) 10 | * If using the `plex_assistant.command` service in HA's Developer Tools is working 11 | * Config options if relevant 12 | * If you're using advanced config, show the config you're using 13 | 14 | ## Enable debug logs for the component 15 | 16 | Add the following to your configuration.yaml file, restart, ask plex assistant to do something, go to your logs and view full logs. 17 | 18 | ```yaml 19 | logger: 20 | default: warning 21 | logs: 22 | custom_components.plex_assistant: debug 23 | ``` 24 | -------------------------------------------------------------------------------- /custom_components/plex_assistant/intent.py: -------------------------------------------------------------------------------- 1 | import homeassistant.helpers.config_validation as cv 2 | from homeassistant.helpers import intent 3 | 4 | from .const import DOMAIN 5 | 6 | 7 | async def async_setup_intents(hass): 8 | intent.async_register(hass, PlexAssistantIntent()) 9 | hass.components.conversation.async_register( 10 | "Plex", 11 | ["Tell Plex to {command}", "{command} with Plex"], 12 | ) 13 | 14 | 15 | class PlexAssistantIntent(intent.IntentHandler): 16 | intent_type = "Plex" 17 | slot_schema = {"command": cv.string} 18 | 19 | async def async_handle(self, intent_obj): 20 | slots = self.async_validate_slots(intent_obj.slots) 21 | if "initialize_plex_intent" in slots["command"]["value"]: 22 | return 23 | await intent_obj.hass.services.async_call(DOMAIN, "command", {"command": slots["command"]["value"]}) 24 | response = intent_obj.create_response() 25 | response.async_set_speech("Sending command to Plex.") 26 | return response 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Ryan Meek 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/label-commenter-config.yml: -------------------------------------------------------------------------------- 1 | labels: 2 | - name: bug 3 | labeled: 4 | issue: 5 | body: | 6 | ## Important: 7 | **Issues that don't provide the information requested in the [troubleshooting docs](https://github.com/maykar/plex_assistant/blob/master/troubleshooting.md) may not get a reply and may be closed until the info is provided. Please, include as much of the requested info as possible so that I can replicate and investigate your issue.**

8 | 9 | **When pasting in your config/code/errors always place 3 backticks ``` above the first line and after the last line. Doing this will format it correctly.**
10 | - name: missing info 11 | labeled: 12 | issue: 13 | body: In order for me to recreate and troubleshoot your issue, please read the bot's previous reply. Follow the post's steps and provide the info requested. This issue will remain closed until more information is provided. 14 | action: close 15 | - name: stale 16 | labeled: 17 | issue: 18 | body: There has been no recent activity on this issue and/or more info was requested and not given. Please, ask for the issue to be reopened if it hasn't been resolved and provide the requested info. 19 | action: close 20 | - name: duplicate 21 | labeled: 22 | issue: 23 | body: This issue is a duplicate of another issue. Please, search open and closed issues before posting. 24 | action: close 25 | -------------------------------------------------------------------------------- /custom_components/plex_assistant/plex_assistant.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from functools import lru_cache 3 | 4 | 5 | class PlexAssistant: 6 | def __init__(self, server, start_script_keys): 7 | self.server = server 8 | self.library = self.server.library 9 | self.devices = {} 10 | self.start_script_keys = start_script_keys 11 | self.tv_id = self.get_section_id("show") 12 | self.movie_id = self.get_section_id("movie") 13 | self.music_id = self.get_section_id("artist") 14 | 15 | @property 16 | def device_names(self): 17 | names = list(self.devices.keys()) + self.start_script_keys 18 | return list(dict.fromkeys(names)) 19 | 20 | @property 21 | def section_id(self): 22 | return { 23 | "movie": self.movie_id, 24 | "show": self.tv_id, 25 | "season": self.tv_id, 26 | "episode": self.tv_id, 27 | "artist": self.music_id, 28 | "album": self.music_id, 29 | "track": self.music_id, 30 | } 31 | 32 | @property 33 | @lru_cache() 34 | def media(self): 35 | media_items = {"all_titles": []} 36 | for item in ["show", "movie", "artist", "album", "track"]: 37 | media_items[f"{item}_titles"] = [x.title for x in self.library.search(libtype=item, sort="addedAt:desc")] 38 | media_items["all_titles"] += media_items[f"{item}_titles"] 39 | media_items["playlist_titles"] = [x.title for x in self.server.playlists()] 40 | media_items["updated"] = datetime.now() 41 | return media_items 42 | 43 | def get_section_id(self, section): 44 | section = self.library.search(libtype=section, limit=1) 45 | return None if not section else section[0].librarySectionID 46 | -------------------------------------------------------------------------------- /custom_components/plex_assistant/translations/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "step": { 4 | "user": { 5 | "title": "❱ Plex Assistant Setup", 6 | "description": "[Documentation](https://github.com/maykar/plex_assistant) | [Forums](https://community.home-assistant.io/t/plex-assistant/) | [Troubleshooting](https://github.com/maykar/plex_assistant/blob/master/troubleshooting.md) | [Support Development](https://github.com/sponsors/maykar)", 7 | "data": { 8 | "server_name": "Select Plex server", 9 | "language": "Language", 10 | "default_cast": "Default cast device", 11 | "tts_errors": "Speak errors on cast device" 12 | } 13 | } 14 | }, 15 | "abort": { 16 | "single_instance_allowed": "Only a single instance is allowed.", 17 | "no_plex_server": "No Plex servers were found, be sure that the Home Assistant Plex integration is setup and working.", 18 | "ha_ver_unsupported": "This Plex Assistant version requires Home Assistant 2021.2.0 or higher." 19 | } 20 | }, 21 | "options": { 22 | "step": { 23 | "init": { 24 | "title": "❱ Plex Assistant Advanced Options", 25 | "description": "[Documentation](https://github.com/maykar/plex_assistant) | [Forums](https://community.home-assistant.io/t/plex-assistant/) | [Troubleshooting](https://github.com/maykar/plex_assistant/blob/master/troubleshooting.md) | [Support Development](https://github.com/sponsors/maykar)\n\n**Read the [Advanced Config Docs](https://github.com/maykar/plex_assistant/tree/master#advanced-configuration) for how to format of the next 2 options.**", 26 | "data": { 27 | "start_script": "Run a script to start an unavailable Plex client", 28 | "keyword_replace": "Replace keywords or phrases with your own", 29 | "jump_f": "Default jump forward amount in seconds.", 30 | "jump_b": "Default jump backward amount in seconds." 31 | } 32 | } 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /custom_components/plex_assistant/translations/hu.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "step": { 4 | "user": { 5 | "title": "❱ Plex Assistant Beállítás", 6 | "description": "[Dokumentáció](https://github.com/maykar/plex_assistant) | [Fórumok](https://community.home-assistant.io/t/plex-assistant/) | [Hibakeresés](https://github.com/maykar/plex_assistant/blob/master/troubleshooting.md) | [Fejlesztés Támogatása](https://github.com/sponsors/maykar)", 7 | "data": { 8 | "server_name": "Kérem válasszon Plex szervert", 9 | "language": "Nyelv", 10 | "default_cast": "Alapértelmezett cast eszköz", 11 | "tts_errors": "TTS (beszédszintézis) problémák a cast eszközön" 12 | } 13 | } 14 | }, 15 | "abort": { 16 | "single_instance_allowed": "Csak egy példány engedélyezett.", 17 | "no_plex_server": "Nem található Plex szerver, ellenőrizze, hogy a Home Assistant Plex integrációja engedélyezve van és működik.", 18 | "ha_ver_unsupported": "Ezen Plex Assistant verzió futtatásához Home Assistant 2021.2.0 vagy magasabb verzió szükséges." 19 | } 20 | }, 21 | "options": { 22 | "step": { 23 | "init": { 24 | "title": "❱ Plex Assistant Haladó Beállítások", 25 | "description": "[Dokumentáció](https://github.com/maykar/plex_assistant) | [Fórumok](https://community.home-assistant.io/t/plex-assistant/) | [Hibakeresés](https://github.com/maykar/plex_assistant/blob/master/troubleshooting.md) | [Fejlesztés Támogatása](https://github.com/sponsors/maykar)\n\n**Read the [Advanced Config Docs](https://github.com/maykar/plex_assistant/tree/master#advanced-configuration) for how to format of the next 2 options.**", 26 | "data": { 27 | "start_script": "Szkript futtatása egy elérhetetlen Plex kliens indításához", 28 | "keyword_replace": "Cseréld le a kulcsszavakat a sajátjaidra", 29 | "jump_f": "Alapértelmezett előretekerés másodpercben.", 30 | "jump_b": "Alapértelmezett visszatekerés másodpercben." 31 | } 32 | } 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /custom_components/plex_assistant/translations/it.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "step": { 4 | "user": { 5 | "title": "❱ Configurazione di Plex Assistant", 6 | "description": "[Documentazione](https://github.com/maykar/plex_assistant/) | [Forum](https://community.home-assistant.io/t/plex-assistant/) | [Soluzione ai problemi](https://github.com/maykar/plex_assistant/blob/master/troubleshooting.md) | [Supporto Sviluppo](https://github.com/sponsors/maykar)", 7 | "data": { 8 | "server_name": "Selezione il server Plex", 9 | "language": "Lingua", 10 | "default_cast": "Dispositivo per il cast predefinito", 11 | "tts_errors": "Errori sul dispositivo cast" 12 | } 13 | } 14 | }, 15 | "abort": { 16 | "single_instance_allowed": "Permessa solo una istanza.", 17 | "no_plex_server": "ServeR Plex non trovato, assicurati che Plex Assistant sia configurato e funzionante.", 18 | "ha_ver_unsupported": "Questa versione di Plex Assistant richiede versione di Home Assistant 2021.2.0 o superiore." 19 | } 20 | }, 21 | "options": { 22 | "step": { 23 | "init": { 24 | "title": "❱ Opzioni avanzate di Plex Assistant", 25 | "description": "[Documentazione](https://github.com/maykar/plex_assistant) | [Forum](https://community.home-assistant.io/t/plex-assistant/) | [Soluzione ai problemi](https://github.com/maykar/plex_assistant/blob/master/troubleshooting.md) | [Supporto Sviluppo](https://github.com/sponsors/maykar)\n\n**Read the [Manuale di configurazione avanzaao](https://github.com/maykar/plex_assistant/tree/master#advanced-configuration) for how to format of the next 2 options.**", 26 | "data": { 27 | "start_script": "Esegui uno script per far partire un Client Plex non disponibile", 28 | "keyword_replace": "Rimpiazza parole o frasi con le tue personali", 29 | "jump_f": "Quantita predefinita di secondi per l'avanzamento.", 30 | "jump_b": "Quantita predefinita di secondi per riavvolgere." 31 | } 32 | } 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /custom_components/plex_assistant/translations/es.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "step": { 4 | "user": { 5 | "title": "❱ Configuración del Plex Assistant", 6 | "description": "[Documentación](https://github.com/maykar/plex_assistant/) | [Foros](https://community.home-assistant.io/t/plex-assistant/) | [Solución de problemas](https://github.com/maykar/plex_assistant/blob/master/troubleshooting.md) | [Support Development](https://github.com/sponsors/maykar)", 7 | "data": { 8 | "server_name": "Seleccione el servidor Plex", 9 | "language": "Idioma", 10 | "default_cast": "Dispositivo cast predeterminado", 11 | "tts_errors": "Habla errores en el dispositivo cast" 12 | } 13 | } 14 | }, 15 | "abort": { 16 | "single_instance_allowed": "Solo se permite una instancia.", 17 | "no_plex_server": "No se encontraron servidores Plex, asegúrese de que la integración de Plex en Home Assistant esté configurada y funcionando.", 18 | "ha_ver_unsupported": "Esta versión del Plex Assistant requiere una version 2021.2.0 o mayor de Home Assistant." 19 | } 20 | }, 21 | "options": { 22 | "step": { 23 | "init": { 24 | "title": "❱ Opciones avanzadas del Plex Assistant", 25 | "description": "[Documentación](https://github.com/maykar/plex_assistant) | [Foros](https://community.home-assistant.io/t/plex-assistant/) | [Solución de problemas](https://github.com/maykar/plex_assistant/blob/master/troubleshooting.md) | [Support Development](https://github.com/sponsors/maykar)\n\n**Read the [Advanced Config Docs](https://github.com/maykar/plex_assistant/tree/master#advanced-configuration) for how to format of the next 2 options.**", 26 | "data": { 27 | "start_script": "Ejecute un script para iniciar un cliente Plex no disponible", 28 | "keyword_replace": "Reemplace palabras claves o frases con las suyas", 29 | "jump_f": "Cantidad predeterminada de segundos para avanzar.", 30 | "jump_b": "Cantidad predeterminada de segundos para retroceder." 31 | } 32 | } 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /custom_components/plex_assistant/config_flow.py: -------------------------------------------------------------------------------- 1 | from homeassistant import config_entries 2 | from homeassistant.core import callback 3 | 4 | import voluptuous as vol 5 | 6 | from .const import DOMAIN, HA_VER_SUPPORTED 7 | from .localize import translations 8 | 9 | 10 | def get_devices(_self): 11 | devices = [] 12 | for entity in list(_self.hass.data["media_player"].entities): 13 | info = str(entity.device_info["identifiers"]) if entity.device_info else "" 14 | if "plex" in info or "cast" in info: 15 | try: 16 | devices.append(_self.hass.states.get(entity.entity_id).attributes.get("friendly_name")) 17 | except AttributeError: 18 | continue 19 | else: 20 | continue 21 | return devices 22 | 23 | 24 | def get_servers(_self): 25 | try: 26 | return [x.title for x in _self.hass.config_entries.async_entries("plex")] 27 | except (KeyError, AttributeError): 28 | return [] 29 | 30 | 31 | def get_schema(_self): 32 | multi_server_schema = {vol.Optional("server_name"): vol.In(_self.servers)} 33 | default_schema = { 34 | vol.Optional("language", default="en"): vol.In(translations.keys()), 35 | vol.Optional("default_cast"): vol.In(get_devices(_self)), 36 | vol.Optional("tts_errors", default=True): bool, 37 | } 38 | return {**multi_server_schema, **default_schema} if len(_self.servers) > 1 else default_schema 39 | 40 | 41 | class PlexAssistantFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): 42 | VERSION = 1 43 | CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL 44 | 45 | @staticmethod 46 | @callback 47 | def async_get_options_flow(config_entry): 48 | return PlexAssistantOptionsFlowHandler(config_entry) 49 | 50 | def __init__(self): 51 | self.servers = None 52 | 53 | async def async_step_user(self, user_input=None): 54 | self.servers = get_servers(self) 55 | 56 | if self._async_current_entries(): 57 | return self.async_abort(reason="single_instance_allowed") 58 | if not HA_VER_SUPPORTED: 59 | return self.async_abort(reason="ha_ver_unsupported") 60 | if len(self.servers) < 1: 61 | return self.async_abort(reason="no_plex_server") 62 | if user_input is not None: 63 | server = user_input["server_name"] if "server_name" in user_input else self.servers[0] 64 | return self.async_create_entry(title=server, data=user_input) 65 | 66 | return await self._show_config_form(user_input) 67 | 68 | async def _show_config_form(self, user_input): 69 | return self.async_show_form( 70 | step_id="user", 71 | data_schema=vol.Schema(get_schema(self)), 72 | ) 73 | 74 | 75 | class PlexAssistantOptionsFlowHandler(config_entries.OptionsFlow): 76 | def __init__(self, config_entry): 77 | self.config_entry = config_entry 78 | self.options = dict(config_entry.options) 79 | 80 | async def async_step_init(self, user_input=None): 81 | if user_input is not None: 82 | self.options.update(user_input) 83 | return await self._update_options() 84 | 85 | return self.async_show_form( 86 | step_id="init", 87 | data_schema=vol.Schema( 88 | { 89 | vol.Optional( 90 | "start_script", 91 | description={"suggested_value": self.options.get("start_script", "")}, 92 | default="", 93 | ): str, 94 | vol.Optional( 95 | "keyword_replace", 96 | description={"suggested_value": self.options.get("keyword_replace", "")}, 97 | default="", 98 | ): str, 99 | vol.Required("jump_f", default=self.options.get("jump_f", 30)): int, 100 | vol.Required("jump_b", default=self.options.get("jump_b", 15)): int, 101 | } 102 | ), 103 | ) 104 | 105 | async def _update_options(self): 106 | return self.async_create_entry(title="", data=self.options) 107 | -------------------------------------------------------------------------------- /ver_one_update.md: -------------------------------------------------------------------------------- 1 | # Plex Assistant Version 1.0.0 2 | ### Version 1.0.0 requires Home Assistant 2021.2.0 or higher. 3 | 4 | **This release has many new features, improvements, and breaking changes.** 5 | 6 | # Breaking Changes 7 | 8 | The automation, intent, intent script, and sensor are no longer required to be setup by the user.
The component handles all of it automatically and there is no need for a sensor anymore.

9 | This means you need to remove those items from HA.
If you don't remove them it may cause issues and duplicate commands.

10 | Configuration is now handled in the UI so the old config method is no longer needed as well. 11 | 12 | ### Remove these things if they exist: 13 | 14 |
15 | Plex Assistant Config 16 | 17 | Remove your entire plex assistant config, including `plex_assistant:` 18 | 19 | ```yaml 20 | plex_assistant: 21 | url: 'http://192.168.1.3:32400' 22 | token: 'tH1s1Sy0uRT0k3n' 23 | default_cast: 'Downstairs TV' 24 | language: 'en' 25 | tts_errors: true 26 | aliases: 27 | Downstairs TV: TV0565124 28 | Upstairs TV: Samsung_66585 29 | ``` 30 | 31 |
32 | 33 |
34 | Sensor 35 | 36 | Remove the `plex_assistant` sensor. 37 | 38 | ```yaml 39 | sensor: # Keep this line if other sensors are listed below it. 40 | - platform: plex_assistant 41 | ``` 42 | 43 |
44 | 45 |
46 | IFTTT Automation 47 | 48 | ```yaml 49 | alias: Plex Assistant Automation 50 | trigger: 51 | - platform: event 52 | event_type: ifttt_webhook_received 53 | event_data: 54 | action: call_service 55 | condition: 56 | condition: template 57 | value_template: "{{ trigger.event.data.service == 'plex_assistant.command' }}" 58 | action: 59 | - service: "{{ trigger.event.data.service }}" 60 | data: 61 | command: "{{ trigger.event.data.command }}" 62 | ``` 63 | 64 |
65 | 66 |
67 | Intent 68 | 69 | Keep `conversation:` and keep `intents:` if you have other intents. 70 | 71 | ```yaml 72 | conversation: #### Keep this line 73 | intents: #### and this one if you have other intents. 74 | Plex: 75 | - "Tell Plex to {command}" 76 | - "{command} with Plex" 77 | ``` 78 | 79 |
80 | 81 |
82 | Intent Script 83 | 84 | ```yaml 85 | intent_script: # Keep this line if you have other intent scripts below 86 | Plex: 87 | speech: 88 | text: "Command sent to Plex." 89 | action: 90 | - service: plex_assistant.command 91 | data: 92 | command: "{{command}}" 93 | ``` 94 | 95 |
96 | 97 | 98 | # Configuration 99 | 100 | **You need to have Home Assistant's [Plex integration](https://www.home-assistant.io/integrations/plex/) setup in order to use Plex Assistant.**

101 | This will help with configuration and support, will allow for more improvements as Plex Assistant progresses, and requires less processing than the old method. 102 | 103 | Configuration is now handled in the UI. [Find the configuration docs in the updated readme.](https://github.com/maykar/plex_assistant#configuration) 104 | 105 | 106 | # New Features 107 | 108 | * Configuration through the UI 109 | * HA's `media_player` entities are now used for devices 110 | * The entity's friendly name is now used for it's device name 111 | * Media automatically resumes where you left off 112 | * Continuous play when playing a show, ondeck, latest, etc. 113 | * New commands "skip forward" and "skip back" to go to next or previous media item 114 | * New random command to play a random item or selected items in a random order 115 | * New "Start Script" feature to start Plex Clients that aren't currently open 116 | * Replace any word or phrase in command with a replacement of your preference 117 | * Jump forward/back works for all devices and amount (in seconds) is configurable 118 | * Component listens for IFTTT and DialogFlow calls automatically (no more intents, scripts, or automations needed) 119 | * Roman numerals are now handled 120 | * DialogFlow instructions improved with uploadable template action 121 | * Remote server support 122 | 123 | # Thank you 124 | 125 | For patiently answering my endless questions and putting up with my constant pestering/suggestions, I'd like to give a huge "Thank you!" to two individuals for their help in getting Plex Assistant to this point. 126 | 127 | [@jjlawren](https://github.com/jjlawren) for their guidance and their work on Python-PlexAPI and HA's Plex integration. The ability to use media player entities, continuous playing of media items, and more wouldn't be possible without them. 128 | 129 | [@ludeeus](https://github.com/ludeeus) for HACS, help with config flow, guidance on all things python, and always being willing to help. 130 | -------------------------------------------------------------------------------- /custom_components/plex_assistant/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Plex Assistant is a component for Home Assistant to add control of Plex to 3 | Google Assistant with a little help from IFTTT or DialogFlow. 4 | 5 | Play to Google Cast devices or Plex Clients using fuzzy searches for media and 6 | cast device names. 7 | 8 | https://github.com/maykar/plex_assistant 9 | """ 10 | 11 | from homeassistant.config_entries import ConfigEntry 12 | from homeassistant.core import Config, HomeAssistant 13 | from homeassistant.components.zeroconf import async_get_instance 14 | 15 | import os 16 | 17 | from .const import DOMAIN, _LOGGER 18 | from .plex_assistant import PlexAssistant 19 | from .process_speech import ProcessSpeech 20 | from .localize import translations 21 | from .helpers import ( 22 | filter_media, 23 | find_media, 24 | fuzzy, 25 | get_devices, 26 | get_server, 27 | listeners, 28 | media_error, 29 | media_service, 30 | no_device_error, 31 | play_tts_error, 32 | process_config_item, 33 | remote_control, 34 | run_start_script, 35 | seek_to_offset, 36 | ) 37 | 38 | 39 | async def async_setup(hass: HomeAssistant, config: Config): 40 | if DOMAIN in config: 41 | changes_url = "https://github.com/maykar/plex_assistant/blob/master/ver_one_update.md" 42 | message = ( 43 | "Configuration is now handled in the UI, please read the %s for how to migrate " 44 | "to the new version and more info.%s " 45 | ) 46 | service_data = { 47 | "title": "Plex Assistant Breaking Changes", 48 | "message": message % (f"[change log]({changes_url})", "."), 49 | } 50 | await hass.services.async_call("persistent_notification", "create", service_data, False) 51 | _LOGGER.warning("Plex Assistant: " + message % ("change log", f". {changes_url}")) 52 | return True 53 | 54 | 55 | async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): 56 | if hass.data.get(DOMAIN) is None: 57 | hass.data.setdefault(DOMAIN, {}) 58 | 59 | server_name = entry.data.get("server_name") 60 | default_device = entry.data.get("default_cast") 61 | tts_errors = entry.data.get("tts_errors") 62 | lang = entry.data.get("language") 63 | localize = translations[lang] 64 | start_script = process_config_item(entry.options, "start_script") 65 | keyword_replace = process_config_item(entry.options, "keyword_replace") 66 | jump_amount = [entry.options.get("jump_f") or 30, entry.options.get("jump_b") or 15] 67 | zeroconf = await async_get_instance(hass) 68 | 69 | server = await get_server(hass, hass.config, server_name) 70 | if not server: 71 | return True 72 | 73 | def pa_executor(_server, start_script_keys): 74 | _pa = PlexAssistant(_server, start_script_keys) 75 | get_devices(hass, _pa) 76 | _LOGGER.debug(f"Media titles: {len(_pa.media['all_titles'])}") 77 | return _pa 78 | 79 | pa = await hass.async_add_executor_job(pa_executor, server, list(start_script.keys())) 80 | 81 | tts_dir = hass.config.path() + "/www/plex_assist_tts/" 82 | if tts_errors and not os.path.exists(tts_dir): 83 | os.makedirs(tts_dir, mode=0o777) 84 | 85 | ifttt_listener = await listeners(hass) 86 | hass.data[DOMAIN][entry.entry_id] = {"remove_listener": ifttt_listener} 87 | entry.add_update_listener(async_reload_entry) 88 | 89 | def handle_input(call): 90 | hass.services.async_call("plex", "scan_for_clients", blocking=False, limit=30) 91 | command = call.data.get("command").strip() 92 | media = None 93 | 94 | if not command: 95 | _LOGGER.warning(localize["no_call"]) 96 | return 97 | _LOGGER.debug("Command: %s", command) 98 | 99 | command = command.lower() 100 | if keyword_replace and any(keyword.lower() in command for keyword in keyword_replace.keys()): 101 | for keyword in keyword_replace.keys(): 102 | command = command.replace(keyword.lower(), keyword_replace[keyword].lower()) 103 | 104 | get_devices(hass, pa) 105 | command = ProcessSpeech(pa, localize, command, default_device).results 106 | _LOGGER.debug("Processed Command: %s", {i: command[i] for i in command if i != "library" and command[i]}) 107 | 108 | if not command["device"] and not default_device: 109 | no_device_error(localize) 110 | return 111 | 112 | if pa.media["updated"] < pa.library.search(sort="addedAt:desc", limit=1)[0].addedAt: 113 | type(pa).media.fget.cache_clear() 114 | _LOGGER.debug(f"Updated Library: {pa.media['updated']}") 115 | 116 | device = fuzzy(command["device"] or default_device, pa.device_names) 117 | device = run_start_script(hass, pa, command, start_script, device, default_device) 118 | 119 | _LOGGER.debug("PA Devices: %s", pa.devices) 120 | if device[1] < 60: 121 | no_device_error(localize, command["device"]) 122 | return 123 | _LOGGER.debug("Device: %s", device[0]) 124 | 125 | device = pa.devices[device[0]] 126 | 127 | if command["control"]: 128 | remote_control(hass, zeroconf, command["control"], device, jump_amount) 129 | return 130 | 131 | media, library = find_media(pa, command) 132 | media, offset = filter_media(pa, command, media, library) 133 | 134 | if not media: 135 | error = media_error(command, localize) 136 | _LOGGER.warning(error) 137 | if tts_errors and device["device_type"] != "plex": 138 | play_tts_error(hass, tts_dir, device["entity_id"], error, lang) 139 | return 140 | 141 | _LOGGER.debug("Media: %s", str(media.items)) 142 | 143 | payload = '%s{"playqueue_id": %s, "type": "%s", "plex_server": "%s"}' % ( 144 | "plex://" if device["device_type"] in ["cast", "sonos"] else "", 145 | media.playQueueID, 146 | media.playQueueType, 147 | server._server.friendlyName, 148 | ) 149 | 150 | media_service(hass, device["entity_id"], "play_media", payload) 151 | seek_to_offset(hass, offset, device["entity_id"]) 152 | 153 | hass.services.async_register(DOMAIN, "command", handle_input) 154 | return True 155 | 156 | 157 | async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: 158 | try: 159 | hass.data[DOMAIN][entry.entry_id]["remove_listener"]() 160 | except KeyError: 161 | pass 162 | hass.services.async_remove(DOMAIN, "command") 163 | hass.data[DOMAIN].pop(entry.entry_id) 164 | return True 165 | 166 | 167 | async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: 168 | await async_unload_entry(hass, entry) 169 | await async_setup_entry(hass, entry) 170 | -------------------------------------------------------------------------------- /translation.md: -------------------------------------------------------------------------------- 1 | # Translation 2 | 3 | ### Contributing to translations 4 | 5 | Translations are located in the [localize.py](https://github.com/maykar/plex_assistant/blob/master/custom_components/plex_assistant/localize.py) file. 6 | 7 | Translations are held in a dictionary with the language code as the key (in this case "en"): 8 | 9 | Any item can accept a list of words/phrases like the example below. The only exceptions are oridnal numbers and error messages, they only accept on word/phrase: 10 | 11 | ``` 12 | "next_track": [ 13 | "go to next track", 14 | "go to next", 15 | "skip to next track", 16 | "skip to next", 17 | "skip forward", 18 | "next track", 19 | "next", 20 | "skip", 21 | ], 22 | ``` 23 | 24 | ### Generic Terms 25 | 26 | The first grouping of "Generic Terms" are translations of generic words that would be used throughout. 27 | For example in `"play": "play"` the first "play" is the key and should not be changed and the second "play" is the translation of the word. 28 | 29 | The keys "movies" and "shows" contain a list of keywords that would inform us of the media type the user is looking for. 30 | 31 | ``` 32 | # Generic Terms 33 | "play": [ 34 | "play", 35 | ], 36 | "random": [ 37 | "random", 38 | "shuffle", 39 | ], 40 | "movies": [ 41 | "movie", 42 | "film", 43 | ], 44 | "shows": [ 45 | "episode", 46 | "tv", 47 | "show" 48 | ], 49 | ``` 50 | 51 | ### Controls 52 | 53 | These are the media player controls. 54 | 55 | ``` 56 | # Controls 57 | "controls": { 58 | "play": [ 59 | "play", 60 | ], 61 | "pause": [ 62 | "pause", 63 | ], 64 | "stop": [ 65 | "stop", 66 | ], 67 | "next_track": [ 68 | "go to next track", 69 | "go to next", 70 | "skip to next track", 71 | "skip to next", 72 | "skip next", 73 | "skip forward", 74 | "next track", 75 | "next", 76 | "skip", 77 | ], 78 | "previous_track": [ 79 | "go to previous track", 80 | "go back one track", 81 | "back one track", 82 | "go back", 83 | "back", 84 | ], 85 | "jump_forward": [ 86 | "jump forward", 87 | "fast forward", 88 | "forward", 89 | ], 90 | "jump_back": [ 91 | "jump back", 92 | "rewind", 93 | ], 94 | }, 95 | ``` 96 | 97 | ### Errors 98 | 99 | Text for errors, use only single items/strings for the translation like in the example below. 100 | 101 | ``` 102 | "not_found": "not found", 103 | "cast_device": "cast device", 104 | "no_call": "No command was received.", 105 | ``` 106 | 107 | ### Invoke Commands 108 | 109 | The next part is an array with the key "play_start". These are phrases of how someone could start the command. 110 | Each of these is tested against "movies" and "shows" from above to decide if the user is looking for a show or a movie. 111 | Once any of the play_start phrases are found they are removed from the command so that they don't add to other options like 112 | the media title, so it is important to have as many ways that it could be phrased included. 113 | ``` 114 | # Invoke Command 115 | "play_start": [ 116 | "play the movie", 117 | "play movie", 118 | "play the tv show", 119 | "play tv show", 120 | "play the show", 121 | "play tv", 122 | "play show", 123 | "play the", 124 | "play" 125 | ], 126 | ``` 127 | 128 | ### Keywords, Pre, and Post 129 | 130 | The rest of the dictionary uses keywords, pre, and post. 131 | * "keywords" are the different ways that someone might say what they are looking for. 132 | * "pre" are words that might preceed the keywords. 133 | * "post" are words that might proceed the keywords. 134 | 135 | Pre and post should be ordered by proximity to the keyword. For for the example with a keyword "latest" and a command of `"play the very latest episode of"` the pre list should be in this order `"very", "the"` and the post list should be in this order `"episode", "of"` (the word "very" isn't actually handled, but just used as an example). 136 | 137 | For example, the english version for latest episode selection looks like this: 138 | ``` 139 | "latest": { 140 | "keywords": [ 141 | "latest", 142 | "recent", 143 | "new", 144 | ], 145 | "pre": [ 146 | "the", 147 | ], 148 | "post": [ 149 | "episode", 150 | "of", 151 | ], 152 | }, 153 | ``` 154 | This will allow the user to say something like `play the latest episode of Friends`, `play latest of Friends`, `play latest Friends`, or `play the latest Friends` 155 | and the options for latest and media type are set, then our command in each case becomes `play Friends`. 156 | 157 | 158 | ### Ordinal Numbers 159 | 160 | The ordinals section is for converting ordinal numbers (`first, second, third...`) into their corrisponding integers (`1, 2, 3...`). I'm not entirely sure how other languages handle ordinals, but this is the only section where you would edit the keys for translation and leave the integers alone. This section also includes "pre" and "post" as above, do not change their keys. 161 | 162 | ``` 163 | # Ordinal Numbers to Integers 164 | "ordinals": { 165 | "first": "1", 166 | "second": "2", 167 | "third": "3", 168 | "fourth": "4", 169 | "fifth": "5", 170 | "sixth": "6", 171 | "seventh": "7", 172 | "eighth": "8", 173 | "ninth": "9", 174 | "tenth": "10", 175 | "pre": [ 176 | "the", 177 | ], 178 | "post": [], 179 | }, 180 | ``` 181 | 182 | Ordinal numbers between 1 and 10 (first and tenth) are often represented as words by Google Assistant, but anything past that is returned as an integer followed by "st", "nd", "rd" or "th" (`31st, 42nd, 23rd, 11th`). The way we would handle those in english would be using the "pre" key like in the "episode" example below. 183 | 184 | ``` 185 | "episode": { 186 | "keywords": [ 187 | "episode", 188 | ], 189 | "pre": [ 190 | 'st', 191 | 'nd', 192 | 'rd', 193 | 'th', 194 | ], 195 | "post": [ 196 | "number", 197 | "of", 198 | ], 199 | }, 200 | ``` 201 | 202 | ### Seperator 203 | 204 | This is the word that seperates the media from cast device. In English it is "on". It operates like the keywords above with a post and pre. The "music_separator" works the same way, but for phrases like `"Play album by artist"`.: 205 | 206 | ``` 207 | "separator": { 208 | # Only use one keyword for this one. 209 | "keywords": [ 210 | "on", 211 | ], 212 | "pre": [], 213 | "post": [ 214 | "the", 215 | ], 216 | }, 217 | ``` 218 | 219 | ### Additional Info 220 | 221 | There is a commented out template at the end of the file that you may copy and paste from. 222 | 223 | Please, also consider translating `custom_components/plex_assistant/translations/en.json` to a new file with the language code replaced in the file name. Example: `custom_components/plex_assistant/translations/nl.json`. This is for the setup and options on HA's integration page. 224 | -------------------------------------------------------------------------------- /custom_components/plex_assistant/process_speech.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from .helpers import fuzzy 4 | 5 | 6 | class ProcessSpeech: 7 | def __init__(self, pa, localize, command, default_cast): 8 | self.pa = pa 9 | self.command = command 10 | self.localize = localize 11 | self.device = default_cast 12 | self.tv_keys = localize["shows"] + localize["season"]["keywords"] + localize["episode"]["keywords"] 13 | self.music_keys = localize["music"] + localize["artists"] + localize["albums"] + localize["tracks"] 14 | self.random = False 15 | self.control = None 16 | self.library = None 17 | self.media = None 18 | self.process_command() 19 | 20 | @property 21 | def results(self): 22 | options = [ 23 | "media", 24 | "device", 25 | "season", 26 | "episode", 27 | "latest", 28 | "unwatched", 29 | "random", 30 | "ondeck", 31 | "control", 32 | "library", 33 | ] 34 | return {option: getattr(self, option, None) for option in options} 35 | 36 | def process_command(self): 37 | controls = self.localize["controls"] 38 | pre_command = self.command 39 | for control in controls: 40 | ctrl = [controls[control]] if isinstance(controls[control], str) else controls[control] 41 | for c in ctrl: 42 | if self.command.startswith(c): 43 | control_check = self.command.replace(c, "").strip() 44 | if control_check == "": 45 | self.control = control 46 | return 47 | device = fuzzy(control_check, self.pa.device_names) 48 | self.find_replace("separator") 49 | if device[0] in ["watched", "deck", "on watched", "on deck"]: 50 | continue 51 | if device[1] > 60 and self.command.replace(device[0].lower(), "").strip() == c: 52 | self.device = device[0] 53 | self.control = control 54 | return 55 | self.command = pre_command 56 | 57 | self.library = self.get_library() 58 | self.find_replace("play_start") 59 | 60 | for item in ["random", "latest", "unwatched", "ondeck"]: 61 | setattr(self, item, self.find_replace(item)) 62 | 63 | for item in ["season", "episode"]: 64 | if self.find_replace(item, False): 65 | self.library = "show" 66 | setattr(self, item, self.get_season_episode_num(self.localize[item])) 67 | self.find_replace(item) 68 | 69 | for item in ["artist", "album", "track"]: 70 | if self.find_replace(f"music_{item}"): 71 | self.library = item 72 | 73 | self.get_media_and_device() 74 | 75 | def get_library(self): 76 | cmd = self.command 77 | for device in self.pa.device_names: 78 | if device.lower() in cmd: 79 | cmd = cmd.replace(device.lower(), "") 80 | 81 | for item in ["shows", "movies", "artists", "albums", "tracks", "playlists"]: 82 | if any(word in cmd for word in self.localize[item]): 83 | return item[:-1] 84 | 85 | if any(word in cmd for word in self.music_keys): 86 | return "track" 87 | if any(word in cmd for word in self.tv_keys): 88 | return "episode" 89 | 90 | def is_device(self, media_list, separator): 91 | split = self.command.split(separator) 92 | full_score = fuzzy(self.command, media_list)[1] 93 | split_score = fuzzy(self.command.replace(split[-1], "")[0], media_list)[1] 94 | cast_score = fuzzy(split[-1], self.pa.device_names)[1] 95 | return full_score < split_score or full_score < cast_score 96 | 97 | def clear_generic(self): 98 | self.find_replace("movies") 99 | self.find_replace("playlists") 100 | for key in self.music_keys + self.tv_keys: 101 | self.command = self.command.replace(key, "") 102 | 103 | def get_media_and_device(self): 104 | for separator in self.localize["separator"]["keywords"]: 105 | if separator in self.command: 106 | self.find_replace("separator", True, separator) 107 | 108 | if self.command.strip().startswith(separator + " "): 109 | self.device = self.command.replace(separator, "").strip() 110 | return 111 | 112 | separator = f" {separator} " 113 | if separator in self.command: 114 | for item in ["show", "movie", "artist", "album", "track", "playlist", "all"]: 115 | if item == "all" or self.library == item: 116 | self.device = self.is_device(self.pa.media[f"{item}_titles"], separator) 117 | 118 | if self.device: 119 | split = self.command.split(separator) 120 | self.command = self.command.replace(separator + split[-1], "") 121 | self.device = split[-1] 122 | 123 | self.clear_generic() 124 | 125 | if self.find_replace("music_separator", False) and getattr(self, "library", None) in [ 126 | "artist", 127 | "album", 128 | "track", 129 | None, 130 | ]: 131 | self.media = self.media_by_artist() or self.command 132 | 133 | if not getattr(self, "media", None): 134 | self.clear_generic() 135 | self.media = self.command 136 | 137 | def media_by_artist(self): 138 | for separator in self.localize["music_separator"]["keywords"]: 139 | if separator in self.command: 140 | self.find_replace("music_separator", True, separator) 141 | split = self.command.split(f" {separator} ") 142 | artist = fuzzy(split[-1], self.pa.media["artist_titles"]) 143 | if artist[1] > 60: 144 | albums = self.pa.server.search(artist[0], "album") 145 | album_titles = [x.title for x in albums] 146 | tracks = self.pa.server.search(artist[0], "track") 147 | track_titles = [x.title for x in tracks] 148 | if not self.library: 149 | artist_item = fuzzy(split[0], album_titles + track_titles) 150 | if artist_item[1] > 60: 151 | return next((x for x in albums + tracks if artist_item[0] in getattr(x, "title", "")), None) 152 | elif self.library == "album": 153 | artist_item = fuzzy(split[0], album_titles) 154 | if artist_item[1] > 60: 155 | return next((x for x in albums if artist_item[0] in getattr(x, "title", "")), None) 156 | elif self.library == "track": 157 | artist_item = fuzzy(split[0], track_titles) 158 | if artist_item[1] > 60: 159 | return next((x for x in tracks if artist_item[0] in getattr(x, "title", "")), None) 160 | return self.command 161 | 162 | def find_replace(self, item, replace=True, replacement=""): 163 | item = self.localize[item] 164 | if isinstance(item, str): 165 | item = {"keywords": [item]} 166 | elif isinstance(item, list): 167 | item = {"keywords": item} 168 | 169 | if all(keyword not in self.command for keyword in item["keywords"]): 170 | return False 171 | 172 | if replace: 173 | if replacement: 174 | replacement = f" {replacement} " 175 | for keyword in item["keywords"]: 176 | self.command = f" {self.command} " 177 | for pre in item.get("pre", []): 178 | self.command = self.command.replace(f"{pre} {keyword}", replacement) 179 | for post in item.get("post", []): 180 | self.command = self.command.replace(f"{keyword} {post}", replacement) 181 | if keyword in self.command: 182 | self.command = self.command.replace(f" {keyword} ", replacement) 183 | self.command = self.command.strip() 184 | self.command = " ".join(self.command.split()) 185 | return True 186 | 187 | def convert_ordinals(self, item): 188 | match = "" 189 | matched = "" 190 | ordinals = self.localize["ordinals"] 191 | for word in item["keywords"]: 192 | for ordinal in ordinals.keys(): 193 | if ordinal not in ("pre", "post") and ordinal in self.command: 194 | match_before = re.search(fr"({ordinal})\s*({word})", self.command) 195 | match_after = re.search(fr"({word})\s*({ordinal})", self.command) 196 | if match_before: 197 | match = match_before 198 | matched = match.group(1) 199 | if match_after: 200 | match = match_after 201 | matched = match.group(2) 202 | if match: 203 | replacement = match.group(0).replace(matched, ordinals[matched]) 204 | self.command = self.command.replace(match.group(0), replacement) 205 | for pre in ordinals["pre"]: 206 | if f"{pre} {match.group(0)}" in self.command: 207 | self.command = self.command.replace(f"{pre} {match.group(0)}", replacement) 208 | for post in ordinals["post"]: 209 | if f"{match.group(0)} {post}" in self.command: 210 | self.command = self.command.replace(f"{match.group(0)} {post}", replacement) 211 | return self.command.strip() 212 | 213 | def get_season_episode_num(self, item): 214 | self.command = self.convert_ordinals(item) 215 | phrase = "" 216 | number = None 217 | for keyword in item["keywords"]: 218 | if keyword in self.command: 219 | phrase = keyword 220 | for pre in item["pre"]: 221 | if pre in self.command: 222 | regex = fr"(\d+\s+)({pre}\s+)({phrase}\s+)" 223 | if re.search(regex, self.command): 224 | self.command = re.sub(regex, fr"{phrase} \1 ", self.command) 225 | else: 226 | self.command = re.sub( 227 | fr"({pre}\s+)({phrase}\s+)(\d+\s+)", 228 | fr"{phrase} \3", 229 | self.command, 230 | ) 231 | self.command = re.sub( 232 | fr"({phrase}\s+)(\d+\s+)({pre}\s+)", 233 | fr"{phrase} \2", 234 | self.command, 235 | ) 236 | for post in item["post"]: 237 | if post in self.command: 238 | regex = fr"({phrase}\s+)({post}\s+)(\d+\s+)" 239 | if re.search(regex, self.command): 240 | self.command = re.sub(regex, fr"{phrase} \3", self.command) 241 | else: 242 | self.command = re.sub( 243 | fr"(\d+\s+)({phrase}\s+)({post}\s+)", 244 | fr"{phrase} \1", 245 | self.command, 246 | ) 247 | self.command = re.sub( 248 | fr"({phrase}\s+)(\d+\s+)({post}\s+)", 249 | fr" {phrase} \2", 250 | self.command, 251 | ) 252 | match = re.search(fr"(\d+)\s*({phrase}|^)|({phrase}|^)\s*(\d+)", self.command) 253 | if match: 254 | number = match.group(1) or match.group(4) 255 | self.command = self.command.replace(match.group(0), "").strip() 256 | return number 257 | -------------------------------------------------------------------------------- /custom_components/plex_assistant/helpers.py: -------------------------------------------------------------------------------- 1 | import re 2 | import time 3 | import uuid 4 | import pychromecast 5 | 6 | from rapidfuzz import fuzz, process 7 | from gtts import gTTS 8 | from json import JSONDecodeError, loads 9 | from homeassistant.components.plex.services import get_plex_server 10 | from homeassistant.exceptions import HomeAssistantError, ServiceNotFound 11 | from homeassistant.core import Context 12 | from pychromecast.controllers.plex import PlexController 13 | 14 | from .const import DOMAIN, _LOGGER 15 | 16 | 17 | def fuzzy(media, lib, scorer=fuzz.QRatio): 18 | if isinstance(lib, list) and len(lib) > 0: 19 | return process.extractOne(media, lib, scorer=scorer) or ["", 0] 20 | return ["", 0] 21 | 22 | 23 | def process_config_item(options, option_type): 24 | option = options.get(option_type) 25 | if not option: 26 | return {} 27 | try: 28 | option = loads("{" + option + "}") 29 | for i in option.keys(): 30 | _LOGGER.debug(f"{option_type} {i}: {option[i]}") 31 | except (TypeError, AttributeError, KeyError, JSONDecodeError): 32 | _LOGGER.warning(f"There is a formatting error in the {option_type.replace('_', ' ')} config.") 33 | option = {} 34 | return option 35 | 36 | 37 | async def get_server(hass, config, server_name): 38 | try: 39 | await hass.helpers.discovery.async_discover(None, None, "plex", config) 40 | return get_plex_server(hass, server_name)._plex_server 41 | except HomeAssistantError as error: 42 | server_name_str = ", the server_name is correct," if server_name else "" 43 | _LOGGER.warning( 44 | f"Plex Assistant: {error.args[0]}. Ensure that you've setup the HA " 45 | f"Plex integration{server_name_str} and the server is reachable. " 46 | ) 47 | 48 | 49 | def get_devices(hass, pa): 50 | for entity in list(hass.data["media_player"].entities): 51 | info = str(entity.device_info.get("identifiers", "")) if entity.device_info else "" 52 | dev_type = [x for x in ["cast", "sonos", "plex", ""] if x in info][0] 53 | if not dev_type: 54 | continue 55 | try: 56 | name = hass.states.get(entity.entity_id).attributes.get("friendly_name") 57 | except AttributeError: 58 | continue 59 | pa.devices[name] = {"entity_id": entity.entity_id, "device_type": dev_type} 60 | 61 | 62 | def run_start_script(hass, pa, command, start_script, device, default_device): 63 | if device[0] in start_script.keys(): 64 | start = hass.data["script"].get_entity(start_script[device[0]]) 65 | start.script.run(context=Context()) 66 | get_devices(hass, pa) 67 | return fuzzy(command["device"] or default_device, list(pa.devices.keys())) 68 | return device 69 | 70 | 71 | async def listeners(hass): 72 | def ifttt_webhook_callback(event): 73 | if event.data["service"] == "plex_assistant.command": 74 | _LOGGER.debug("IFTTT Call: %s", event.data["command"]) 75 | hass.services.call(DOMAIN, "command", {"command": event.data["command"]}) 76 | 77 | listener = hass.bus.async_listen("ifttt_webhook_received", ifttt_webhook_callback) 78 | try: 79 | await hass.services.async_call("conversation", "process", {"text": "tell plex to initialize_plex_intent"}) 80 | except ServiceNotFound: 81 | pass 82 | return listener 83 | 84 | 85 | def media_service(hass, entity_id, call, payload=None): 86 | args = {"entity_id": entity_id} 87 | if call == "play_media": 88 | args = {**args, **{"media_content_type": "video", "media_content_id": payload}} 89 | elif call == "media_seek": 90 | args = {**args, **{"seek_position": payload}} 91 | hass.services.call("media_player", call, args) 92 | 93 | 94 | def jump(hass, device, amount): 95 | if device["device_type"] == "plex": 96 | media_service(hass, device["entity_id"], "media_pause") 97 | time.sleep(0.5) 98 | 99 | offset = hass.states.get(device["entity_id"]).attributes.get("media_position", 0) + amount 100 | media_service(hass, device["entity_id"], "media_seek", offset) 101 | 102 | if device["device_type"] == "plex": 103 | media_service(hass, device["entity_id"], "media_play") 104 | 105 | 106 | def cast_next_prev(hass, zeroconf, plex_c, device, direction): 107 | entity = hass.data["media_player"].get_entity(device["entity_id"]) 108 | cast, browser = pychromecast.get_listed_chromecasts( 109 | uuids=[uuid.UUID(entity._cast_info.uuid)], zeroconf_instance=zeroconf 110 | ) 111 | pychromecast.discovery.stop_discovery(browser) 112 | cast[0].register_handler(plex_c) 113 | cast[0].wait() 114 | if direction == "next": 115 | plex_c.next() 116 | else: 117 | plex_c.previous() 118 | 119 | 120 | def remote_control(hass, zeroconf, control, device, jump_amount): 121 | plex_c = PlexController() 122 | if control == "jump_forward": 123 | jump(hass, device, jump_amount[0]) 124 | elif control == "jump_back": 125 | jump(hass, device, -jump_amount[1]) 126 | elif control == "next_track" and device["device_type"] == "cast": 127 | cast_next_prev(hass, zeroconf, plex_c, device, "next") 128 | elif control == "previous_track" and device["device_type"] == "cast": 129 | cast_next_prev(hass, zeroconf, plex_c, device, "previous") 130 | else: 131 | media_service(hass, device["entity_id"], f"media_{control}") 132 | 133 | 134 | def seek_to_offset(hass, offset, entity): 135 | if offset < 1: 136 | return 137 | timeout = 0 138 | while not hass.states.is_state(entity, "playing") and timeout < 100: 139 | time.sleep(0.10) 140 | timeout += 1 141 | 142 | timeout = 0 143 | if hass.states.is_state(entity, "playing"): 144 | media_service(hass, entity, "media_pause") 145 | while not hass.states.is_state(entity, "paused") and timeout < 100: 146 | time.sleep(0.10) 147 | timeout += 1 148 | 149 | if hass.states.is_state(entity, "paused"): 150 | if hass.states.get(entity).attributes.get("media_position", 0) < 9: 151 | media_service(hass, entity, "media_seek", offset) 152 | media_service(hass, entity, "media_play") 153 | 154 | 155 | def no_device_error(localize, device=None): 156 | device = f': "{device.title()}".' if device else "." 157 | _LOGGER.warning(f"{localize['cast_device'].capitalize()} {localize['not_found']}{device}") 158 | 159 | 160 | def media_error(command, localize): 161 | error = "".join( 162 | f"{localize[keyword]['keywords'][0]} " for keyword in ["latest", "unwatched", "ondeck"] if command[keyword] 163 | ) 164 | if command["media"]: 165 | media = command["media"] 166 | media = media if isinstance(media, str) else getattr(media, "title", str(media)) 167 | error += f"{media.capitalize()} " 168 | elif command["library"]: 169 | error += f"{localize[command['library']+'s'][0]} " 170 | for keyword in ["season", "episode"]: 171 | if command[keyword]: 172 | error += f"{localize[keyword]['keywords'][0]} {command[keyword]} " 173 | error += f"{localize['not_found']}." 174 | return error.capitalize() 175 | 176 | 177 | def play_tts_error(hass, tts_dir, device, error, lang): 178 | tts = gTTS(error, lang=lang) 179 | tts.save(tts_dir + "error.mp3") 180 | hass.services.call( 181 | "media_player", 182 | "play_media", 183 | { 184 | "entity_id": device, 185 | "media_content_type": "audio/mp3", 186 | "media_content_id": "/local/plex_assist_tts/error.mp3", 187 | }, 188 | ) 189 | 190 | 191 | def filter_media(pa, command, media, library): 192 | offset = 0 193 | 194 | if library == "playlist": 195 | media = pa.server.playlist(media) if media else pa.server.playlists() 196 | elif media or library: 197 | media = pa.library.search(title=media or None, libtype=library or None) 198 | 199 | if isinstance(media, list) and len(media) == 1: 200 | media = media[0] 201 | 202 | if command["episode"]: 203 | media = media.episode(season=int(command["season"] or 1), episode=int(command["episode"])) 204 | elif command["season"]: 205 | media = media.season(season=int(command["season"])) 206 | 207 | if command["ondeck"]: 208 | title, libtype = [command["media"], command["library"]] 209 | if getattr(media, "onDeck", None): 210 | media = media.onDeck() 211 | elif title or libtype: 212 | search_result = pa.library.search(title=title or None, libtype=libtype or None, limit=1)[0] 213 | if getattr(search_result, "onDeck", None): 214 | media = search_result.onDeck() 215 | else: 216 | media = pa.library.sectionByID(search_result.librarySectionID).onDeck() 217 | else: 218 | media = pa.library.sectionByID(pa.tv_id).onDeck() + pa.library.sectionByID(pa.movie_id).onDeck() 219 | media.sort(key=lambda x: getattr(x, "addedAt", None), reverse=False) 220 | 221 | if command["unwatched"]: 222 | if isinstance(media, list) or (not media and not library): 223 | media = media[:200] if isinstance(media, list) else pa.library.recentlyAdded() 224 | media = [x for x in media if getattr(x, "viewCount", 0) == 0] 225 | elif getattr(media, "unwatched", None): 226 | media = media.unwatched()[:200] 227 | 228 | if command["latest"] and not command["unwatched"]: 229 | if library and not media and pa.section_id[library]: 230 | media = pa.library.sectionByID(pa.section_id[library]).recentlyAdded()[:200] 231 | elif not media: 232 | media = pa.library.sectionByID(pa.tv_id).recentlyAdded() 233 | media += pa.library.sectionByID(pa.mov_id).recentlyAdded() 234 | media.sort(key=lambda x: getattr(x, "addedAt", None), reverse=True) 235 | media = media[:200] 236 | elif command["latest"]: 237 | if getattr(media, "type", None) in ["show", "season"]: 238 | media = media.episodes()[-1] 239 | elif isinstance(media, list): 240 | media = media[:200] 241 | media.sort(key=lambda x: getattr(x, "addedAt", None), reverse=True) 242 | 243 | if not command["random"] and media: 244 | pos = getattr(media[0], "viewOffset", 0) if isinstance(media, list) else getattr(media, "viewOffset", 0) 245 | offset = (pos / 1000) - 5 if pos > 15 else 0 246 | 247 | if getattr(media, "TYPE", None) == "show": 248 | unwatched = media.unwatched()[:30] 249 | media = unwatched if unwatched and not command["random"] else media.episodes()[:30] 250 | elif getattr(media, "TYPE", None) == "episode": 251 | episodes = media.show().episodes() 252 | episodes = episodes[episodes.index(media) : episodes.index(media) + 30] 253 | media = pa.server.createPlayQueue(episodes, shuffle=int(command["random"])) 254 | elif getattr(media, "TYPE", None) in ["artist", "album"]: 255 | tracks = media.tracks() 256 | media = pa.server.createPlayQueue(tracks, shuffle=int(command["random"])) 257 | elif getattr(media, "TYPE", None) == "track": 258 | tracks = media.album().tracks() 259 | tracks = tracks[tracks.index(media) :] 260 | media = pa.server.createPlayQueue(tracks, shuffle=int(command["random"])) 261 | 262 | if getattr(media, "TYPE", None) != "playqueue" and media: 263 | media = pa.server.createPlayQueue(media, shuffle=int(command["random"])) 264 | 265 | return [media, 0 if media and media.items[0].listType == "audio" else offset] 266 | 267 | 268 | def roman_numeral_test(media, lib): 269 | regex = re.compile(r"\b(\d|(10))\b") 270 | replacements = { 271 | "1": "I", 272 | "2": "II", 273 | "3": "III", 274 | "4": "IV", 275 | "5": "V", 276 | "6": "VI", 277 | "7": "VII", 278 | "8": "VIII", 279 | "9": "IX", 280 | "10": "X", 281 | } 282 | 283 | if len(re.findall(regex, media)) > 0: 284 | replaced = re.sub(regex, lambda m: replacements[m.group(1)], media) 285 | return fuzzy(replaced, lib, fuzz.WRatio) 286 | return ["", 0] 287 | 288 | 289 | def find_media(pa, command): 290 | result = "" 291 | lib = "" 292 | if getattr(command["media"], "type", None) in ["artist", "album", "track"]: 293 | return [command["media"], command["media"].type] 294 | if command["library"]: 295 | lib_titles = pa.media[f"{command['library']}_titles"] 296 | if command["media"]: 297 | result = fuzzy(command["media"], lib_titles, fuzz.WRatio) 298 | roman_test = roman_numeral_test(command["media"], lib_titles) 299 | result = result[0] if result[1] > roman_test[1] else roman_test[0] 300 | elif command["media"]: 301 | item = {} 302 | score = {} 303 | for category in ["show", "movie", "artist", "album", "track", "playlist"]: 304 | lib_titles = pa.media[f"{category}_titles"] 305 | standard = fuzzy(command["media"], lib_titles, fuzz.WRatio) if lib_titles else ["", 0] 306 | roman = roman_numeral_test(command["media"], lib_titles) if lib_titles else ["", 0] 307 | 308 | winner = standard if standard[1] > roman[1] else roman 309 | item[category] = winner[0] 310 | score[category] = winner[1] 311 | 312 | winning_category = max(score, key=score.get) 313 | result = item[winning_category] 314 | lib = winning_category 315 | 316 | return [result, lib or command["library"]] 317 | -------------------------------------------------------------------------------- /OLD_README.md: -------------------------------------------------------------------------------- 1 | # ❱ Plex Assistant 2 | 3 | [![hacs_badge](https://img.shields.io/badge/HACS-Default-yellow.svg)](https://github.com/custom-components/hacs) [![hacs_badge](https://img.shields.io/badge/Buy-Me%20a%20Coffee-critical)](https://www.buymeacoffee.com/FgwNR2l) 4 | 5 | [Installation](#installation) | [Configuration](#configuration) | [Cast Devices](#cast-devices) | [Commands](#commands)
6 | [Google Assistant Triggers](#google-assistant-triggers) | [HA Conversation Setup](#home-assistant-conversation-setup)

7 | 8 | Plex Assistant is a Home Assistant component to allow Google Assistant, Home Assistant's conversation integration, and more to cast Plex media to Google devices and Plex clients. You could use this component with anything that can make a service call to HA as well (see the automations in the [Google Assistant trigger guides](](#google-assistant-triggers)) for IFTTT and DialogFlow as a starting point). 9 | 10 | Example: `"Hey Google, tell Plex to play The Walking Dead on the Downstairs TV."` 11 | 12 | You can use the component's service (`plex_assistant.command`) to call the commands however you'd like. Visit the services tab in HA's Developer Tools to test it out. 13 | 14 | Example [HA service call](https://www.home-assistant.io/docs/scripts/service-calls/): 15 | ``` 16 | service: plex_assistant.command 17 | data: 18 | command: Play Breaking Bad 19 | ``` 20 | 21 | ***Music and audio aren't built in yet, only shows and movies at the moment.*** 22 | 23 | ## Supporting Development 24 | - :coffee:  [Buy me a coffee](https://www.buymeacoffee.com/FgwNR2l) 25 | - :heart:  [Sponsor me on GitHub](https://github.com/sponsors/maykar) 26 | - :keyboard:  Help with [translation](translation.md), development, or documentation 27 |

28 | 29 | ## Installation 30 | Install by using one of the methods below: 31 | 32 | * **Install with [HACS](https://hacs.xyz/):** Search integrations for "Plex Assistant", select and hit install. Add the configuration (see below) to your configuration.yaml file. 33 | 34 | * **Install Manually:** Install this component by copying all of [these files](https://github.com/maykar/plex_assistant/tree/master/custom_components/plex_assistant) to `/custom_components/plex_assistant/`. Add the configuration (see below) to your configuration.yaml file. 35 | 36 | ## Configuration 37 | Add config to your configuration.yaml file. 38 | 39 | | Key | Default | Necessity | Description 40 | | :-- | :------ | :-------- | :---------- 41 | | url | | **Required** | The full url to your Plex instance including port. [Info for SSL connections here](#ssl-url). 42 | | token | | **Required** | Your Plex token. [How to find your Plex token](https://support.plex.tv/articles/204059436-finding-an-authentication-token-x-plex-token/). 43 | | default_cast | | Optional | The name of the cast device to use if none is specified. 44 | | language | 'en' | Optional | Language code ([Supported Languages](#currently-supported-languages)). 45 | | tts_errors | true | Optional | Will speak errors on the selected cast device. For example: when the specified media wasn't found. 46 | | aliases | | Optional | Set alias names for your devices. Example below, set what you want to call it then it's actual name or machine ID. 47 | 48 |
49 | 50 | **Sample Config** 51 | ```yaml 52 | plex_assistant: 53 |   url: 'http://192.168.1.3:32400' 54 |   token: 'tH1s1Sy0uRT0k3n' 55 |   default_cast: 'Downstairs TV' 56 | language: 'en' 57 | tts_errors: true 58 | aliases: 59 | Downstairs TV: TV0565124 60 | Upstairs TV: Samsung_66585 61 | ``` 62 | 63 | ## Cast Devices 64 | This component does not use HA's media_player entities, it automatically detects compatible devices (Google Cast devices and Plex Clients). It will use the name from the devices themselves. Use the [companion sensor](#companion-sensor) to get a list of compatible devices with their names/IDs. 65 | 66 | ## Companion Sensor 67 | 68 | Plex Assistant includes a sensor to display the names of currently connected devices as well as the machine ID of Plex clients. This is to help with config and troubleshooting. 69 | 70 | Add the sensor by including the code below in your configuration.yaml file. 71 | 72 | ```yaml 73 | sensor: 74 | - platform: plex_assistant 75 | ``` 76 | 77 | To update the sensor send the command "update sensor" to Plex Assistant either through your voice assistant (e.g. `"Hey Google, tell Plex to update sensor."`) or as a HA service call. The sensor is also updated any time Plex Assistant is sent a command. To view the sensor results, navigate to "Developer Tools" in HA's sidebar and click "States", then find `sensor.plex_assistant_devices` in the list below. 78 | 79 | Plex clients must be open in order to be detected or recieve commands from this component, Plex can sometimes take around a minute to detect that a client is active/inactive. 80 | 81 | ***You must restart after installation and configuration, you may want to setup Google Assistant triggers or HA's conversation intergration first as they will also require a restart. Instructions for each below.*** 82 | 83 | ## Google Assistant Triggers 84 | 85 | You can either use IFTTT or DialogFlow to trigger Plex Assistant with Google Assistant. IFTTT is the easiest way to set this up, but only if IFTTT supports your language. DialogFlow is a bit more involved and has some quirks, but has support for more languages. 86 | 87 |
88 | IFTTT Setup Guide 89 | 90 | ## IFTTT Setup 91 | 92 | #### In Home Assistant 93 | 94 | * Go to "Configuration" in your HA sidebar and select "Integrations" 95 | * Hit the add button and search for "IFTTT" and click configure. 96 | * Follow the on screen instructions. 97 | * Copy or save the URL that is displayed at the end, we'll need it later and it won't be shown again. 98 | * Click "Finish" 99 | 100 | #### In IFTTT 101 | 102 | Visit [ifttt.com](https://ifttt.com/) and sign up or sign in. 103 | 104 | * Create a new applet 105 | * Click "Add" next to "If This". Search for and select "Google Assistant" 106 | * Select "Say phrase with text ingredient" 107 | 108 | Now you can select how you want to trigger this service, you can select up to 3 ways to invoke it. I use things like `tell plex to $` or `have plex $`. The dollar sign will be the phrase sent to this component. See currently supported [commands below](#commands)). You can also set a response from the Google Assistant if you'd like. Select your language (as long as it's supported, see list above), then hit "Create Trigger" to continue. 109 | 110 | * Click "Add" next to "Then That" 111 | * Search for and select "Webhooks", then select "Make a web request" 112 | * In the URL field enter the webhook URL HA provided you earlier 113 | * Select method "Post" and content type "application/json" 114 | * Then copy and paste the code below into the body field 115 | 116 | `{ "action": "call_service", "service": "plex_assistant.command", "command": "{{TextField}}" }` 117 | 118 | #### In Home Assistant 119 | 120 | Finally, add the automation either by using the YAML code or the Blueprint below: 121 | 122 |
123 | Automation Blueprint 124 | 125 | * Go to "Configuration" in your sidebar 126 | * Click "Blueprints", then "Import Blueprint" 127 | * Paste this into the URL field `https://gist.github.com/maykar/11f46cdfab0562e683557403b2aa88b4` 128 | * Click "Preview Blueprint", then "Import Blueprint" 129 | * Find "Plex Assistant IFTTT Automation" in the list and click "Create Automation" 130 | * Type anything on the last line (HA currently requires any interaction to save) 131 | * Hit "Save" 132 | 133 |
134 | 135 |
136 | Automation YAML 137 | 138 | ```yaml 139 | alias: Plex Assistant Automation 140 | trigger: 141 | - platform: event 142 | event_type: ifttt_webhook_received 143 | event_data: 144 | action: call_service 145 | condition: 146 | condition: template 147 | value_template: "{{ trigger.event.data.service == 'plex_assistant.command' }}" 148 | action: 149 | - service: "{{ trigger.event.data.service }}" 150 | data: 151 | command: "{{ trigger.event.data.command }}" 152 | ``` 153 | 154 |
155 | 156 | If you prefer Node Red to HA's automations, @1h8fulkat has shared a [Node Red Flow](https://github.com/maykar/plex_assistant/issues/34) to do this. 157 | 158 | ***Either refresh your automations or restart after adding the automation.*** 159 | 160 |
161 | 162 |
163 | DialogFlow Setup Guide 164 | 165 | ## DialogFlow Setup 166 | 167 | #### In Home Assistant 168 | 169 | * Go to "Configuration" in your HA sidebar and select "Integrations" 170 | * Hit the add button and search for "Dialogflow". 171 | * Copy or save the URL that is displayed, we'll need it later and it won't be shown again. 172 | * Click "Finish" 173 | 174 | #### In DialogFlow 175 | 176 | Visit https://dialogflow.cloud.google.com/ and sign up or sign in. 177 | Keep going until you get to the "Welcome to Dialogflow!" page with "Create Agent" in the sidebar. 178 | 179 | * Click on Create Agent and Type "Plex_Assistant" as the agent name and select "Create" 180 | * Now select "Fulfillment" in the sidebar and enable "Webhook" 181 | * Enter the "URL" Home Assistant provided us earlier, scroll down and click "Save" 182 | * Now select "Intents" in the sidebar and hit the "Create Intent" button. 183 | * Select "ADD PARAMETERS AND ACTION" and enter "Plex" as the action name. 184 | * Check the checkbox under "Required" 185 | * Under "Parameter Name" put "command", under "Entity" put "@sys.any", and under "Value" put "$command" 186 | * Now click "ADD TRAINING PHRASES" 187 | * Create a phrase and type in "command" 188 | * Then double click on the word "command" you just entered and select "@sys.any:command" 189 | * Scroll to the bottom and expand "Fulfillment" then click "ENABLE FULFILLMENT" 190 | * Turn on "Enable webhook call for this intent" 191 | * Expand "Responses" turn on “Set this intent as end of conversation” 192 | * At the top of the page enter "Plex" for the intent name and hit "Save" 193 | * On the left side of the page hit "Integrations", then "Integration Settings" 194 | * Click the space under "Explicit invocation", select "Plex" 195 | * Type "Plex" in "Implicit invocation" 196 | * You may need to hit the test button and accept terms of service before next step 197 | * Click "Manage assistant app", then "Decide how your action is invoked" 198 | * Under "Display Name" type "Plex" then hit save in the top right (it may give an error, but thats okay). 199 | 200 | #### In Home Assistant 201 | 202 | Add the following to your `configuration.yaml` file 203 | 204 | ```yaml 205 | intent_script: 206 | Plex: 207 | speech: 208 | text: "Command sent to Plex." 209 | action: 210 | - service: plex_assistant.command 211 | data: 212 | command: "{{command}}" 213 | ``` 214 | 215 | You can now trigger Plex Assistant by saying "Hey Google, tell plex to..." or "Hey Google, ask plex to..." 216 | 217 | ***Restart after adding the above.*** 218 | 219 |
220 | 221 | ### Currently Supported Languages: 222 | | Language | Code | IFTTT | DialogFlow | 223 | |:----------|:------:|:--------------------:|:--------------------------:| 224 | |    **Dutch** | `"nl"` | :x: | :heavy_check_mark: | 225 | |    **English** | `"en"` | :heavy_check_mark: | :heavy_check_mark: | 226 | |    **French** | `"fr"` | :heavy_check_mark: | :heavy_check_mark: | 227 | |    **German** | `"de"` | :heavy_check_mark: | :heavy_check_mark: | 228 | |    **Italian** | `"it"` | :heavy_check_mark: | :heavy_check_mark: | 229 | |    **Swedish** | `"sv"` | :x: | :heavy_check_mark: | 230 | |    **Danish** | `"da"` | :x: | :heavy_check_mark: | 231 | 232 | #### [Help add support for more languages.](translation.md)
233 | 234 | ## Home Assistant Conversation Setup 235 | 236 | To use Plex Assistant with Home Assistant's conversation integration simply add the code below to your configuration.yaml file. Using the conversation integration will work with any of the languages from the table above. 237 | 238 | ```yaml 239 | conversation: 240 | intents: 241 | Plex: 242 | # These trigger commands can be changed to suit your needs. 243 | - "Tell Plex to {command}" 244 | - "{command} with Plex" 245 | 246 | intent_script: 247 | Plex: 248 | speech: 249 | text: Command sent to Plex. 250 | action: 251 | service: plex_assistant.command 252 | data: 253 | command: "{{command}}" 254 | ``` 255 | 256 | ## Commands 257 | 258 | #### Fuzzy Matching 259 | A show or movie's title and the Chromecast device used in your phrase are processed using a fuzzy search. Meaning it will select the closest match using your Plex media titles and available cast device names. `"play walk in deed on the dawn tee"` would become `"Play The Walking Dead on the Downstairs TV."`. This even works for partial matches. `play Pets 2` will match `The Secret Life of Pets 2`. 260 | 261 | #### You can say things like: 262 | * `"play the latest episode of Breaking Bad on the Living Room TV"` 263 | * `"play unwatched breaking bad"` 264 | * `"play Breaking Bad"` 265 | * `"play Pets 2 on the Kitchen Chromecast"` 266 | * `"play ondeck"` 267 | * `"play ondeck movies"` 268 | * `"play season 1 episode 3 of The Simpsons"` 269 | * `"play first season second episode of Taskmaster on the Theater System"` 270 | 271 | ### Control Commands: 272 | * `play` 273 | * `pause` 274 | * `stop` 275 | * `jump forward` 276 | * `jump back` 277 | 278 | Be sure to add the name of the device to control commands if it is not the default device. `"stop downstairs tv"`. 279 | 280 | If no cast device is specified in your command, the `default_cast` device set in your config is used. A cast device will only be found if at the end of the command and when preceded with the word `"on"` or words `"on the"`. Example: *"play friends **ON** downstairs tv"* 281 | 282 | I've tried to take into account many different ways that commands could be phrased. If you find a phrase that isn't working and you feel should be implemented, please make an issue. 283 | 284 | ### SSL URL 285 | If you use the Plex server network setting of "Required" for "Secure Connections" and do not provide a custom certificate, you need to use your plex.direct URL in the settings. You can find it using the steps below: 286 | 287 | * Go to https://app.plex.tv/ and sign in. 288 | * Hit the vertical 3 dots in the bottom right of any media item (episode, movie, etc) 289 | * Select "Get Info", then click "View XML" 290 | * The URL field of your browser now contains your plex.direct URL 291 | * Copy everything before "/library" 292 | * It will look something like this: `https://192-168-10-25.xxxxxxxxxxxxxxxxx.plex.direct:32400` 293 | 294 | If you use a custom certificate, use the URL that the certificate is for. 295 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ❱ Plex Assistant 2 | 3 | [![hacs_badge](https://img.shields.io/badge/HACS-Default-yellow.svg)](https://github.com/custom-components/hacs) [![hacs_badge](https://img.shields.io/badge/Buy-Me%20a%20Coffee-critical)](https://www.buymeacoffee.com/FgwNR2l) 4 | 5 | [Installation](#installation) | [Configuration](#configuration) | [Cast Devices](#cast-devices) | [Commands](#commands)
6 | [Google Assistant Setup](#google-assistant-setup) | [HA Conversation Setup](#home-assistant-conversation-setup) | [Advanced Config](#advanced-configuration)

7 | 8 | Plex Assistant is a Home Assistant integration for casting Plex media to Google devices, Sonos devices, and Plex clients with Google Assistant, HA's conversation integration, and more. You can use this component with anything that can make a service call to HA as well. 9 | 10 | Example: `"Hey Google, tell Plex to play The Walking Dead on the Downstairs TV."` 11 | 12 | You can use the component's service (`plex_assistant.command`) to call the commands however you'd like. Visit the services tab in HA's Developer Tools to test it out. 13 | 14 | ## [Troubleshooting and Issues](https://github.com/maykar/plex_assistant/blob/master/troubleshooting.md) 15 | 16 | ## Version 1.0.0+ 17 | 18 | There have been many changes in version 1.0.0, follow the [1.0.0 Update Guide](https://github.com/maykar/plex_assistant/blob/master/ver_one_update.md) if updating from a lower version. 19 | 20 | This version requires Home Assistant 2021.2.0+. Use [version 0.3.4](https://github.com/maykar/plex_assistant/releases/tag/0.3.4) if you are on lower versions of HA and find the [old readme here](https://github.com/maykar/plex_assistant/blob/master/OLD_README.md). 21 | 22 | ## Supporting Development 23 | - :coffee:  [Buy me a coffee](https://www.buymeacoffee.com/FgwNR2l) 24 | - :heart:  [Sponsor me on GitHub](https://github.com/sponsors/maykar) 25 | - :keyboard:  Help with [translation](translation.md), development, or documentation 26 | 27 | ## Installation 28 | Install by using one of the methods below: 29 | 30 | * **Install with [HACS](https://hacs.xyz/):** Search integrations for "Plex Assistant", select it, hit install, and restart. 31 | 32 | * **Install Manually:** Install this component by downloading the project and then copying the `/custom_components/plex_assistant/` folder to the `custom_components` folder in your config directory (create the folder if it doesn't exist) and restart. 33 | 34 | ## Configuration 35 | **You need to have [HA's Plex integration](https://www.home-assistant.io/integrations/plex/) setup in order to use Plex Assistant.**
36 | 37 | If you want a Plex Client as your default device, make sure it is open/reachable before setup. 38 | 39 | * In your sidebar click "Configuration" 40 | * Go to "Integrations" and click "Add Integration" 41 | * Search for "Plex Assistant" and click it 42 | * Follow the steps shown to select intial config options 43 | 44 | Your Plex server is automatically retrieved from Home Assistant's Plex integration, if you have more than one server setup it will ask which one to use. 45 | 46 | After setup you can click "Options" on Plex Assistant's card for more config options including: jump forward/back amount and [Advanced Config Options](#advanced-configuration). 47 | 48 | ## Cast Devices 49 | This component automatically detects compatible media_player entities from Home Assistant (Google Cast devices, Sonos devices, and Plex clients). Setting a default device will use that device if none is specified in the command. Plex Assistant uses the friendly name from the entities for commands. To change a Plex client's friendly name in HA it needs to be open and reachable before doing so. 50 | 51 | ## Google Assistant Setup 52 | 53 | You can either use IFTTT or DialogFlow to trigger Plex Assistant with Google Assistant. 54 | 55 | * IFTTT is the easiest way to set this up, but only if IFTTT supports your language. 56 | * DialogFlow is a bit more involved and has some quirks, like always responding "I'm starting the test version of Plex", but it has support for more languages. Only use DialogFlow if your language is otherwise unsupported. 57 | 58 |
59 | IFTTT Setup Guide 60 | 61 | ## IFTTT Setup 62 | 63 | #### In Home Assistant 64 | 65 | * Go to "Configuration" in your HA sidebar and select "Integrations" 66 | * Hit the add button and search for "IFTTT" and click configure. 67 | * Follow the on screen instructions. 68 | * Copy or save the URL that is displayed at the end, we'll need it later. 69 | * Click "Finish" 70 | 71 | #### In IFTTT 72 | 73 | Visit [ifttt.com](https://ifttt.com/) and sign up or sign in. 74 | 75 | * Create a new applet 76 | * Click "Add" next to "If This". 77 | * Search for and select "Google Assistant" 78 | * Select "Say phrase with text ingredient" 79 | 80 | Now you can select how you want to trigger this service, you can select up to 3 ways to invoke it. I use things like `tell plex to $` or `have plex $`. The dollar sign will be the phrase sent to this component. You can also set a response from the Google Assistant if you'd like. Select your language (as long as it's supported, see list above), then hit "Create Trigger" to continue. 81 | 82 | * Click "Add" next to "Then That" 83 | * Search for and select "Webhooks", then select "Make a web request" 84 | * In the URL field enter the webhook URL HA provided you earlier 85 | * Select method "Post" and content type "application/json" 86 | * Then copy and paste the code below into the body field 87 | 88 | `{ "action": "call_service", "service": "plex_assistant.command", "command": "{{TextField}}" }` 89 | 90 | Finally click "Create Action", then "Continue", and then "Finish". 91 | 92 | You can now trigger Plex Assistant by saying "Hey Google, tell plex to..." or "Hey Google, ask plex to..." 93 | 94 |
95 | 96 |
97 | DialogFlow Setup Guide 98 | 99 | ## DialogFlow Setup 100 | 101 | #### In Home Assistant 102 | 103 | The DialogFlow trigger requires Home Assistant's [Conversation integration](https://www.home-assistant.io/integrations/conversation/) to be enabled. 104 | 105 | * Go to "Configuration" in your HA sidebar and select "Integrations" 106 | * Hit the add button and search for "Dialogflow". 107 | * Copy or save the URL that is displayed, we'll need it later. 108 | * Click "Finish" 109 | 110 | #### In DialogFlow 111 | 112 | Download [Plex_Assistant_DialogFlow.zip](https://github.com/maykar/plex_assistant/raw/master/Plex_Assistant_DialogFlow.zip) and then visit https://dialogflow.cloud.google.com . Sign up or sign in using the same Google account tied to your Google Assistant. Keep going until you get to the "Welcome to Dialogflow!" page with "Create Agent" in the sidebar. 113 | 114 | * Click on Create Agent and Type "Plex" as the agent name and hit "Create" 115 | * Now click the settings icon next to "Plex" in the sidebar 116 | * Navigate to "Export and Import" and click "Restore from ZIP" 117 | * Select the `Plex_Assistant_DialogFlow.zip` file we downloaded earlier and restore 118 | * Click "Fulfillment" in the sidebar and change the URL to the one HA gave us for DialogFlow 119 | * Scroll down and hit "Save" 120 | 121 | If you will be using English as your language you can ignore the next group of steps. 122 | 123 | * To add your language click the plus sign in the sidebar next to "en" 124 | * Select your language under "English - en" and hit "Save" in the top right 125 | * Click your language code next to "en" in the sidebar 126 | * Click "Intents" in the sidebar and then click the "Plex" intent 127 | * In the "Training phrases" section type "command" 128 | * Double click on the word "command" that you just entered and select "@sys.any:command" 129 | * Hit "Save" in the top right. 130 | 131 | If you would like to add a response for the assistant to say after your command: 132 | 133 | * Click "Intents" in the sidebar and then click the "Plex" intent 134 | * In "Responses" write the desired result under "Text Response" 135 | * Hit "Save" in the top right. 136 | 137 | Next you need to publish a test version: 138 | 139 | * Click "Integrations" 140 | * Click "Not ready yet? Continue with the _integration_" in the top panel. 141 | * You should see a dialog shown. Click the 'Test' button. 142 | 143 | You can now trigger Plex Assistant by saying "Hey Google, tell plex to..." or "Hey Google, ask plex to..." 144 | 145 |
146 | 147 | ### Currently Supported Languages: 148 | | Language | Code | IFTTT | DialogFlow | Music Support | 149 | |:---------|:------:|:--------------------:|:--------------------------:|:--------------------------:| 150 | |   **Danish**|`"da"`|:x:|:heavy_check_mark:|:x:| 151 | |   **Dutch**|`"nl"`|:x:|:heavy_check_mark:|:x:| 152 | |   **English**|`"en"`|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:| 153 | |   **French**|`"fr"`|:heavy_check_mark:|:heavy_check_mark:|:x:| 154 | |   **German**|`"de"`|:heavy_check_mark:|:heavy_check_mark:|:x:| 155 | |   **Hungarian**|`"hu"`|:x:|:heavy_check_mark:|:heavy_check_mark:| 156 | |   **Italian**|`"it"`|:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:| 157 | |   **Norwegian**|`"nb"`|:x:|:heavy_check_mark:|:x:| 158 | |   **Portuguese**|`"pt"`|:x:|:heavy_check_mark:|:heavy_check_mark:| 159 | |   **Spanish**|`"es"`|:heavy_check_mark:|:heavy_check_mark:|:x:| 160 | |   **Swedish**|`"sv"`|:x:|:heavy_check_mark:|:x:| 161 | 162 | #### [Help add or improve support for more languages.](translation.md)
163 | 164 | ## Home Assistant Conversation Setup 165 | 166 | Requires Home Assistant's [Conversation integration](https://www.home-assistant.io/integrations/conversation/) to be enabled. 167 | 168 | By default Plex Assistant will work with HA's Conversation integration with the phrases `"Tell Plex to {command}"` and `"{command} with Plex"` with no additional configuration nessisary. All the languages in the table above are supported, but you'd need to make a trigger phrase in your language. If you would like to add more trigger phrases you can do so by using the code below as an example. 169 | 170 | ```yaml 171 | conversation: 172 | intents: 173 | Plex: 174 | - "Plex please would you {command}" 175 | - "I command plex to {command}" 176 | ``` 177 | 178 | ## Commands 179 | 180 | #### Fuzzy Matching 181 | A media item's title and the device used in your phrase are processed using a fuzzy search. Meaning it will select the closest match using your Plex media titles and available cast device names. `"play walk in deed on the dawn tee"` would become `"Play The Walking Dead on the Downstairs TV."`. This even works for partial matches. `play Pets 2` will match `The Secret Life of Pets 2`. 182 | 183 | If no season/episode is specified for a TV show Plex Assistant will play the first unwatched or first in progress episode by default. If an artist, album, or track share the same name it will assume artist first, then album, then track. You can always specify by saying "Play album `album name`", "Play artist...", "Play track...", or even combine those with an artists name: "Play Never Gonna Give You Up **by** Rick Astley" or "Play the **album** Whenever You Need Somebody **by** Rick Astley". This can help with artists having a self titled album or track as well as multiple artists having items with the same name. 184 | 185 | #### You can say things like: 186 | * `"play the latest episode of Breaking Bad on the Living Room TV"` 187 | * `"play Breaking Bad"` 188 | * `"play Add it Up by the Violent Femmes"` 189 | * `"play the track Time to Pretend"` 190 | * `"play the album Time to Pretend by MGMT"` 191 | * `"play ondeck"` 192 | * `"play random unwatched TV"` 193 | * `"play season 1 episode 3 of The Simpsons"` 194 | * `"play the first season second episode of Taskmaster on the Theater System"` 195 | 196 | ### Filter Keywords: 197 | * `season, episode, movie, show` 198 | * `artist, album, track, playlist` 199 | * `latest, recent, new` 200 | * `unwatched, next` 201 | * `ondeck` 202 | * `random, shuffle, randomized, shuffled` 203 | 204 | Filter keywords can be combined. For example `"play random unwatched movies"` will start playing a list of all unwatched movies in random order. 205 | 206 | ### Control Commands: 207 | * `play` 208 | * `pause` 209 | * `stop` 210 | * `next, skip, next track, skip forward` 211 | * `previous, back, go back` 212 | * `jump forward, fast forward, forward` 213 | * `jump back, rewind` 214 | 215 | Be sure to add the name of the device to control commands if it is not the default device. `"stop downstairs tv"` or `"previous on the livingroom tv"`. 216 | 217 | If no cast device is specified in your command, the default device set in your config is used. A cast device will only be found if at the end of the command and when preceded with the word `"on"` or words `"on the"`. Example: *"play friends **ON** downstairs tv"* 218 | 219 | Control commands are the only ones that don't require the `"on"` or `"on the"` before the device name. 220 | 221 | I've tried to take into account many different ways that commands could be phrased. If you find a phrase that isn't working and you feel should be implemented, please make an issue or give the keyword replacement option a try (see below). 222 | 223 | ## Advanced Configuration 224 | 225 | There are two advanced configuration options: keyword replacements and start scripts. HA's UI configuration doesn't have a good way to impliment these kinds of options yet, so formatting is very important for these options. Once there is a better way to handle these I will update the UI. 226 | 227 | ## Keyword Replacements 228 | 229 | This option could be used for a few different purposes. The formatting is the word/phrase you want to say in quotes followed by a colon and then the word/phrase you want replace it with in quotes. Seperate multiple replacements with a comma. 230 | 231 | Here's an example to add to the commands "next" and "previous" with alternatives: 232 | ``` 233 | "full speed ahead":"next", "reverse full power":"previous" 234 | ``` 235 | Using this config would allow you to say "full speed ahead" to go to the next track and "reverse full power" to go to the previous. You can still use the default commands as well. 236 | 237 | Another use example would be if you have multiple Star Trek series, but want a specific one to play when you just say "Star Trek": 238 | ``` 239 | "star trek":"star trek the next generation" 240 | ``` 241 | 242 | And yet another use would be to improve translations, for example: If there are unsupported feminine and masculine variations for your language you can add them yourself. I would also encourage you to create an issue or [help improve translations](translation.md) if you run into a situation like this. 243 | 244 | ## Start Scripts 245 | 246 | This option will trigger a script to start a Plex client if it is currently unavailable. For example: You have a Roku with the Plex app, but need it to be open for Plex Assistant to control it.

The formatting needed is the friendly name of the client that you want to open in quotes (case sensitive) followed by a colon then the HA script to start the client in quotes. Seperate multiple entries with a comma. 247 | ``` 248 | "LivingRoom TV":"script.start_lr_plex", "Bedroom TV":"script.open_br_plex" 249 | ``` 250 | The script would be different for every device and some devices might not have the ability to do this.
251 | Plex Assistant will wait for the start script to finish before continuing, so having a check for device availability is advisable. That way the script can both wait for the device to be available or quickly end if it already is.

252 | The example below would start the Plex app on a Roku device.
The script waits until the app is open on the device and the app reports as available (take note of the comments in the code). 253 | 254 | ``` 255 | roku_plex: 256 | sequence: 257 | - choose: 258 | #### If Plex is already open on the device, do nothing 259 | - conditions: 260 | - condition: template 261 | value_template: >- 262 | {{ state_attr('media_player.roku','source') == 'Plex - Stream for Free' }} 263 | sequence: [] 264 | default: 265 | #### If Plex isn't open on the device, open it 266 | #### You could even add a service to turn your TV on here 267 | - service: media_player.select_source 268 | entity_id: 'media_player.roku' 269 | data: 270 | source: 'Plex - Stream for Free' 271 | - repeat: 272 | #### Wait until the Plex App/Client is available 273 | while: 274 | - condition: template 275 | #### Loop until Plex App or client report as available and stop after 20 tries 276 | value_template: >- 277 | {{ (state_attr('media_player.roku','source') != 'Plex - Stream for Free' or 278 | is_state('media_player.plex_plex_for_roku_roku', 'unavailable')) and 279 | repeat.index <= 20 }} 280 | sequence: 281 | #### Scan to update device status 282 | - service: plex.scan_for_clients 283 | - delay: 284 | seconds: 1 285 | #### Optional delay after device is found. Uncomment the 2 lines for delay below 286 | #### if your device needs a few seconds to respond to commands. Increase delay as needed. 287 | # - delay: 288 | # seconds: 3 289 | mode: single 290 | ``` 291 | -------------------------------------------------------------------------------- /custom_components/plex_assistant/localize.py: -------------------------------------------------------------------------------- 1 | # fmt: off 2 | translations = { 3 | "en": { 4 | # Generic Terms 5 | "play": [ 6 | "play", 7 | ], 8 | "random": [ 9 | "randomized", 10 | "random", 11 | "shuffled", 12 | "shuffle", 13 | ], 14 | "movies": [ 15 | "movies", 16 | "films", 17 | "movie", 18 | "film", 19 | ], 20 | "shows": [ 21 | "episodes", 22 | "shows", 23 | "episode", 24 | "tv", 25 | "show", 26 | ], 27 | "tracks": [ 28 | "tracks", 29 | "track", 30 | "songs", 31 | "song", 32 | "albums", 33 | "album", 34 | "records", 35 | "record", 36 | ], 37 | "albums": [ 38 | "album", 39 | "record", 40 | "cd", 41 | "vinyl", 42 | ], 43 | "artists": [ 44 | "band", 45 | "artist", 46 | "singer", 47 | "composer", 48 | "player", 49 | "guitarist", 50 | ], 51 | "music": [ 52 | "music", 53 | ], 54 | "playlists": [ 55 | "playlist", 56 | "list", 57 | ], 58 | 59 | # Controls 60 | "controls": { 61 | "play": [ 62 | "play", 63 | ], 64 | "pause": [ 65 | "pause", 66 | ], 67 | "stop": [ 68 | "stop", 69 | ], 70 | "next_track": [ 71 | "go to next track", 72 | "go to next", 73 | "skip to next track", 74 | "skip to next", 75 | "skip next", 76 | "skip forward", 77 | "next track", 78 | "next", 79 | "skip", 80 | ], 81 | "previous_track": [ 82 | "go to previous track", 83 | "go back one track", 84 | "back one track", 85 | "go back", 86 | "back", 87 | ], 88 | "jump_forward": [ 89 | "jump forward", 90 | "fast forward", 91 | "forward", 92 | ], 93 | "jump_back": [ 94 | "jump back", 95 | "rewind", 96 | ], 97 | }, 98 | 99 | # Text for errors 100 | "not_found": "not found", 101 | "cast_device": "cast device", 102 | "no_call": "No command was received.", 103 | 104 | # Invoke Command 105 | "play_start": [ 106 | "play the movie", 107 | "play movie", 108 | "play the tv show", 109 | "play tv show", 110 | "play the show", 111 | "play tv", 112 | "play show", 113 | "play the", 114 | "play an", 115 | "play a", 116 | "play", 117 | ], 118 | 119 | # Ordinal Numbers to Integers 120 | "ordinals": { 121 | # Edit the keys for translation, not the integers. 122 | "first": "1", 123 | "second": "2", 124 | "third": "3", 125 | "fourth": "4", 126 | "fifth": "5", 127 | "sixth": "6", 128 | "seventh": "7", 129 | "eighth": "8", 130 | "ninth": "9", 131 | "tenth": "10", 132 | # Do not edit the keys of pre and post 133 | "pre": [ 134 | "the", 135 | ], 136 | "post": [], 137 | }, 138 | 139 | # Keywords, Pre, and Post 140 | "season": { 141 | "keywords": [ 142 | "season", 143 | ], 144 | "pre": [ 145 | 'st', 146 | 'nd', 147 | 'rd', 148 | 'th', 149 | 'the', 150 | ], 151 | "post": [ 152 | "number", 153 | "of", 154 | ], 155 | }, 156 | "episode": { 157 | "keywords": [ 158 | "episodes", 159 | "episode", 160 | ], 161 | "pre": [ 162 | 'st', 163 | 'nd', 164 | 'rd', 165 | 'th', 166 | 'the', 167 | ], 168 | "post": [ 169 | "number", 170 | "of", 171 | ], 172 | }, 173 | "latest": { 174 | "keywords": [ 175 | "latest", 176 | "recent", 177 | "new", 178 | ], 179 | "pre": [ 180 | "the", 181 | ], 182 | "post": [ 183 | "movies", 184 | "movie", 185 | "episodes", 186 | "episode", 187 | "tv", 188 | "shows", 189 | "show", 190 | "of", 191 | ], 192 | }, 193 | "unwatched": { 194 | "keywords": [ 195 | "unwatched", 196 | "on watched", 197 | "next", 198 | ], 199 | "pre": [], 200 | "post": [ 201 | "movies", 202 | "movie", 203 | "episodes", 204 | "episode", 205 | "tv", 206 | "shows", 207 | "show", 208 | "of", 209 | ], 210 | }, 211 | "ondeck": { 212 | "keywords": [ 213 | "ondeck", 214 | "on deck", 215 | ], 216 | "pre": [], 217 | "post": [ 218 | "movies", 219 | "movie", 220 | "episodes", 221 | "episode", 222 | "tv", 223 | "shows", 224 | "show", 225 | "of", 226 | ], 227 | }, 228 | "music_album": { 229 | "keywords": [ 230 | "album", 231 | "record", 232 | ], 233 | "pre": [], 234 | "post": [], 235 | }, 236 | "music_artist": { 237 | "keywords": [ 238 | "artists", 239 | "artist", 240 | "band", 241 | ], 242 | "pre": [], 243 | "post": [], 244 | }, 245 | "music_track": { 246 | "keywords": [ 247 | "track", 248 | "song", 249 | ], 250 | "pre": [], 251 | "post": [], 252 | }, 253 | # This is the separator word used at the end of the command 254 | # To let us know it is a cast device. 255 | # Examples: "Play Coco on Samsung TV" or "Play Coco on the Samsung TV" 256 | "separator": { 257 | "keywords": [ 258 | "on", 259 | ], 260 | "pre": [], 261 | "post": [ 262 | "the", 263 | ], 264 | }, 265 | # This is the separator word used in a command for music 266 | # that narrows down a search by using the artists name. 267 | # Examples: "Play album by artist" or "Play track by the artist" 268 | "music_separator": { 269 | "keywords": [ 270 | "by", 271 | ], 272 | "pre": [], 273 | "post": [ 274 | "the band", 275 | "the artists", 276 | "the artist", 277 | "the group", 278 | "the", 279 | ], 280 | }, 281 | }, 282 | "hu": { 283 | "play": [ 284 | "lejátszás", 285 | "indít", 286 | ], 287 | "random": [ 288 | "véletlenszerű", 289 | "random", 290 | "véletlen szerű", 291 | "véletlen", 292 | ], 293 | "movies": [ 294 | "filmek", 295 | "mozifilmek", 296 | "film", 297 | "mozifilm", 298 | ], 299 | "shows": [ 300 | "epizódok", 301 | "részek", 302 | "rész", 303 | "sorozatok", 304 | "epizód", 305 | "tévé", 306 | "tv", 307 | "sorozat", 308 | ], 309 | "tracks": [ 310 | "számok", 311 | "szám", 312 | "zenék", 313 | "zene", 314 | "zeneszámok", 315 | "zeneszám", 316 | "albumok", 317 | "album", 318 | "lemezek", 319 | "lemez", 320 | "dalok", 321 | "dal", 322 | ], 323 | "albums": [ 324 | "albumok", 325 | "album", 326 | "cédé", 327 | "cd", 328 | "lemez", 329 | ], 330 | "artists": [ 331 | "együttes", 332 | "előadó", 333 | "énekes", 334 | "író", 335 | "zenész", 336 | "gitáros", 337 | ], 338 | "music": [ 339 | "zene", 340 | ], 341 | "playlists": [ 342 | "lejátszási lista", 343 | "lista", 344 | ], 345 | 346 | "controls": { 347 | "play": [ 348 | "lejátszás", 349 | "indítás", 350 | ], 351 | "pause": [ 352 | "pillanat állj", 353 | ], 354 | "stop": [ 355 | "állj", 356 | "stop", 357 | ], 358 | "next_track": [ 359 | "következő szám", 360 | "következő", 361 | "ugrás", 362 | "ugrás a következőre", 363 | "ugrás a következő számra", 364 | "következő zene", 365 | ], 366 | "previous_track": [ 367 | "előző szám", 368 | "előző", 369 | "visszalépés", 370 | "ugrás vissza", 371 | "vissza ugrás", 372 | "ugrás az előzőre", 373 | "ugrás az előző számra", 374 | "előző zene", 375 | 376 | ], 377 | "jump_forward": [ 378 | "előre tekerés", 379 | "előre", 380 | "tekerés", 381 | ], 382 | "jump_back": [ 383 | "vissza tekerés", 384 | "vissza", 385 | ], 386 | }, 387 | 388 | "not_found": "nem található", 389 | "cast_device": "küldés eszközre", 390 | "no_call": "Nem érkezett parancs.", 391 | 392 | "play_start": [ 393 | "játszd le a filmet", 394 | "film lejátszása", 395 | "film indítása", 396 | "indítsd a filmet", 397 | "játszd le a sorozatot", 398 | "sorozat lejátszása", 399 | "sorozat indítása", 400 | "indítsd a sorozatot", 401 | "játszd le a sorozatot", 402 | "lejátszás", 403 | "indítsd", 404 | "indítás", 405 | "játszd le", 406 | "indítsd el", 407 | ], 408 | 409 | "ordinals": { 410 | "első": "1", 411 | "második": "2", 412 | "harmadik": "3", 413 | "negyedik": "4", 414 | "ötödik": "5", 415 | "hatodik": "6", 416 | "hetedik": "7", 417 | "nyolcadik": "8", 418 | "kilencedik": "9", 419 | "tizedik": "10", 420 | "pre": [ 421 | "a", 422 | ], 423 | "post": [], 424 | }, 425 | 426 | "season": { 427 | "keywords": [ 428 | "évad", 429 | "év", 430 | ], 431 | "pre": [ 432 | '.', 433 | 'a', 434 | 'az', 435 | ], 436 | "post": [ 437 | "számú", 438 | ], 439 | }, 440 | "episode": { 441 | "keywords": [ 442 | "epizódok", 443 | "epizód", 444 | "részek", 445 | "rész", 446 | ], 447 | "pre": [ 448 | '.', 449 | 'a', 450 | 'az', 451 | ], 452 | "post": [ 453 | "számú", 454 | ], 455 | }, 456 | "latest": { 457 | "keywords": [ 458 | "legfrissebb", 459 | "utolsó", 460 | "legutolsó", 461 | "új", 462 | "legújabb", 463 | ], 464 | "pre": [ 465 | "a", 466 | "az", 467 | ], 468 | "post": [ 469 | "filmek", 470 | "film", 471 | "epizódok", 472 | "epizód", 473 | "részek", 474 | "rész", 475 | "tv", 476 | "tévé", 477 | "sorozatok", 478 | "sorozat", 479 | ], 480 | }, 481 | "unwatched": { 482 | "keywords": [ 483 | "nem nézett", 484 | "meg nem nézett", 485 | "nem játszott", 486 | ], 487 | "pre": [], 488 | "post": [ 489 | "filmek", 490 | "film", 491 | "epizódok", 492 | "epizód", 493 | "részek", 494 | "rész", 495 | "tv", 496 | "tévé", 497 | "sorozatok", 498 | "sorozat", 499 | ], 500 | }, 501 | "ondeck": { 502 | "keywords": [ 503 | "lejátszás folytatása", 504 | "folytatás", 505 | ], 506 | "pre": [], 507 | "post": [ 508 | "filmek", 509 | "film", 510 | "epizódok", 511 | "epizód", 512 | "részek", 513 | "rész", 514 | "tv", 515 | "tévé", 516 | "sorozatok", 517 | "sorozat", 518 | ], 519 | }, 520 | "music_album": { 521 | "keywords": [ 522 | "album", 523 | "albumok", 524 | "zenei album", 525 | "zenei albumok", 526 | ], 527 | "pre": [], 528 | "post": [], 529 | }, 530 | "music_artist": { 531 | "keywords": [ 532 | "előadó", 533 | "együttes", 534 | "csapat", 535 | ], 536 | "pre": [], 537 | "post": [], 538 | }, 539 | "music_track": { 540 | "keywords": [ 541 | "zene", 542 | "szám", 543 | "zeneszám", 544 | ], 545 | "pre": [], 546 | "post": [], 547 | }, 548 | "separator": { 549 | "keywords": [ 550 | "itt", 551 | ], 552 | "pre": [], 553 | "post": [ 554 | "a", 555 | "az", 556 | ], 557 | }, 558 | "music_separator": { 559 | "keywords": [ 560 | "tőle", 561 | "tőlük", 562 | "ettől", 563 | "ezektől", 564 | ], 565 | "pre": [], 566 | "post": [ 567 | "az együttestől", 568 | "az előadótól", 569 | "az énekestől", 570 | "a bandától", 571 | "a", 572 | "az", 573 | ], 574 | }, 575 | }, 576 | "sv": { 577 | "play": [ 578 | "spela", 579 | ], 580 | "random": [ 581 | "slumpmässigt", 582 | ], 583 | "not_found": "finns ej", 584 | "cast_device": "cast enhet", 585 | "movies": [ 586 | "filmen", 587 | "film", 588 | ], 589 | "shows": [ 590 | "avsnitt", 591 | "tv programmet", 592 | "tv", 593 | "serien", 594 | "serie", 595 | ], 596 | "tracks": [], 597 | "albums": [], 598 | "artists": [], 599 | "music": [], 600 | "playlists": [], 601 | 602 | "controls": { 603 | "play": [ 604 | "spela", 605 | ], 606 | "pause": [ 607 | "paus", 608 | ], 609 | "stop": [ 610 | "sluta", 611 | ], 612 | "next_track": [ 613 | "nästa", 614 | ], 615 | "previous_track": [ 616 | "föregående", 617 | ], 618 | "jump_forward": [ 619 | "hoppa framåt", 620 | ], 621 | "jump_back": [ 622 | "hoppa tillbaka", 623 | ], 624 | }, 625 | "no_call": "Inget kommando mottogs.", 626 | 627 | "play_start": [ 628 | "spela filmen", 629 | "spela film", 630 | "spela tv programmet", 631 | "spela serien", 632 | "spela serie", 633 | "spela tv", 634 | "spela", 635 | ], 636 | 637 | "ordinals": { 638 | "första": "1", 639 | "andra": "2", 640 | "tredje": "3", 641 | "fjärde": "4", 642 | "femte": "5", 643 | "sjätte": "6", 644 | "sjunde": "7", 645 | "åttonde": "8", 646 | "nionde": "9", 647 | "tionde": "10", 648 | "pre": [], 649 | "post": [], 650 | }, 651 | 652 | "season": { 653 | "keywords": [ 654 | "season", 655 | "säsong", 656 | ], 657 | "pre": [ 658 | "a", 659 | "e", 660 | ], 661 | "post": [ 662 | "nummer", 663 | "av", 664 | ], 665 | }, 666 | "episode": { 667 | "keywords": [ 668 | "episod", 669 | "avsnitt", 670 | "episode", 671 | ], 672 | "pre": [ 673 | "a", 674 | "e", 675 | ], 676 | "post": [ 677 | "nummer", 678 | "av", 679 | ], 680 | }, 681 | "latest": { 682 | "keywords": [ 683 | "senaste", 684 | "nyligen", 685 | "ny", 686 | ], 687 | "pre": [ 688 | "det", 689 | ], 690 | "post": [ 691 | "avsnitt", 692 | "av", 693 | ], 694 | }, 695 | "unwatched": { 696 | "keywords": [ 697 | "osedda", 698 | ], 699 | "pre": [], 700 | "post": [ 701 | "avsnitt", 702 | "av", 703 | ], 704 | }, 705 | "ondeck": { 706 | "keywords": [ 707 | "kommande", 708 | ], 709 | "pre": [], 710 | "post": [ 711 | "filmer", 712 | "film", 713 | "TV program", 714 | "program", 715 | "tv", 716 | ], 717 | }, 718 | "music_album": { 719 | "keywords": [], 720 | "pre": [], 721 | "post": [], 722 | }, 723 | "music_artist": { 724 | "keywords": [], 725 | "pre": [], 726 | "post": [], 727 | }, 728 | "music_track": { 729 | "keywords": [], 730 | "pre": [], 731 | "post": [], 732 | }, 733 | 734 | "separator": { 735 | "keywords": [ 736 | "på", 737 | ], 738 | "pre": [], 739 | "post": [], 740 | }, 741 | "music_separator": { 742 | "keywords": [], 743 | "pre": [], 744 | "post": [], 745 | }, 746 | }, 747 | "nl": { 748 | "play": [ 749 | "speel", 750 | "afspelen", 751 | ], 752 | "random": [ 753 | "willekeurige", 754 | "willekeurig", 755 | ], 756 | "movies": [ 757 | "film", 758 | ], 759 | "shows": [ 760 | "aflevering", 761 | "serie", 762 | "show", 763 | ], 764 | "tracks": [ 765 | "lied", 766 | "liedjes", 767 | "nummer", 768 | "nummers", 769 | "muziek", 770 | ], 771 | "albums": [ 772 | "album", 773 | "albums", 774 | ], 775 | "artists": [ 776 | "artiest", 777 | "artiesten", 778 | ], 779 | "music": [ 780 | "muziek", 781 | ], 782 | "playlists": [ 783 | "playlist", 784 | "playlists", 785 | "afspeellijst", 786 | "afspeellijsten", 787 | ], 788 | 789 | "controls": { 790 | "play": [ 791 | "speel", 792 | "afspelen", 793 | ], 794 | "pause": [ 795 | "pauzeer", 796 | ], 797 | "stop": [ 798 | "stop", 799 | ], 800 | "next_track": [ 801 | "volgende", 802 | "volgend", 803 | ], 804 | "previous_track": [ 805 | "vorige" 806 | ], 807 | "jump_forward": [ 808 | "spring naar voren", 809 | "doorspoelen", 810 | ], 811 | "jump_back": [ 812 | "spring naar achter", 813 | "terugspoelen", 814 | ], 815 | }, 816 | 817 | "not_found": "niet gevonden", 818 | "cast_device": "cast apparaat", 819 | "no_call": "Er is geen opdracht ontvangen.", 820 | 821 | "play_start": [ 822 | "speel de film", 823 | "speel film", 824 | "speelfilm", 825 | "speel de aflevering", 826 | "speel aflevering", 827 | "speel de", 828 | "speel", 829 | ], 830 | 831 | "ordinals": { 832 | "eerste": "1", 833 | "tweede": "2", 834 | "derde": "3", 835 | "vierde": "4", 836 | "vijfde": "5", 837 | "zesde": "6", 838 | "zevende": "7", 839 | "achtste": "8", 840 | "negende": "9", 841 | "tiende": "10", 842 | "pre": [ 843 | "de", 844 | ], 845 | "post": [], 846 | }, 847 | 848 | "season": { 849 | "keywords": [ 850 | "seizoen", 851 | ], 852 | "pre": [ 853 | 'ste', 854 | 'de', 855 | ], 856 | "post": [ 857 | "nummer", 858 | "van", 859 | ], 860 | }, 861 | "episode": { 862 | "keywords": [ 863 | "aflevering", 864 | ], 865 | "pre": [ 866 | 'ste', 867 | 'de', 868 | ], 869 | "post": [ 870 | "nummer", 871 | "van", 872 | ], 873 | }, 874 | "latest": { 875 | "keywords": [ 876 | "laatste", 877 | "recentste", 878 | "nieuwste", 879 | ], 880 | "pre": [ 881 | "de", 882 | ], 883 | "post": [ 884 | "aflevering", 885 | "van", 886 | ], 887 | }, 888 | "unwatched": { 889 | "keywords": [ 890 | "niet bekeken", 891 | "onbekeken" 892 | ], 893 | "pre": [], 894 | "post": [ 895 | "afleveringen", 896 | "aflevering", 897 | "van", 898 | ], 899 | }, 900 | "ondeck": { 901 | "keywords": [ 902 | "aan dek", 903 | "voorpagina", 904 | "hoofdpagina", 905 | "beginpagina", 906 | ], 907 | "pre": [ 908 | "de", 909 | "op", 910 | ], 911 | "post": [ 912 | "films", 913 | "film", 914 | "afleveringen", 915 | "aflevering", 916 | ], 917 | }, 918 | "music_album": { 919 | "keywords": [ 920 | "album", 921 | ], 922 | "pre": [ 923 | "het", 924 | ], 925 | "post": [ 926 | "ste", 927 | "de", 928 | ], 929 | }, 930 | "music_artist": { 931 | "keywords": [ 932 | "artiest", 933 | "band", 934 | "groep", 935 | "popgroep", 936 | "rockband" 937 | ], 938 | "pre": [ 939 | "van", 940 | "de", 941 | ], 942 | "post": [ 943 | "te", 944 | ], 945 | }, 946 | "music_track": { 947 | "keywords": [ 948 | "nummer", 949 | ], 950 | "pre": [ 951 | "de", 952 | "het", 953 | ], 954 | "post": [ 955 | "ste", 956 | "de", 957 | ], 958 | }, 959 | 960 | "separator": { 961 | "keywords": [ 962 | "op", 963 | "via", 964 | ], 965 | "pre": [], 966 | "post": [ 967 | "de", 968 | ], 969 | }, 970 | "music_separator": { 971 | "keywords": [ 972 | "van", 973 | "door", 974 | ], 975 | "pre": [], 976 | "post": [ 977 | "de band", 978 | "de artiest", 979 | "de groep", 980 | "door de band", 981 | "door de artiest", 982 | "door de groep", 983 | ], 984 | }, 985 | }, 986 | "it": { 987 | "play": [ 988 | "riproduci", 989 | ], 990 | "random": [ 991 | "casuale", 992 | ], 993 | "movies": [ 994 | "film", 995 | ], 996 | "shows": [ 997 | "episodio", 998 | "telefilm", 999 | "serie tv", 1000 | ], 1001 | "tracks": [], 1002 | "albums": [], 1003 | "artists": [], 1004 | "music": [], 1005 | "playlists": [], 1006 | 1007 | "controls": { 1008 | "play": [ 1009 | "riproduci", 1010 | ], 1011 | "pause": [ 1012 | "metti in pasa", 1013 | ], 1014 | "stop": [ 1015 | "interrompi", 1016 | ], 1017 | "next_track": [ 1018 | "successivo", 1019 | ], 1020 | "previous_track": [ 1021 | "precedente", 1022 | ], 1023 | "jump_forward": [ 1024 | "vai avanti", 1025 | ], 1026 | "jump_back": [ 1027 | "vai indietro", 1028 | ], 1029 | }, 1030 | 1031 | "not_found": "non trovato", 1032 | "cast_device": "dispositivo cast", 1033 | "no_call": "Nessun comando ricevuto.", 1034 | 1035 | "play_start": [ 1036 | "riproduci il film", 1037 | "riproduci film", 1038 | "riproduci la serie tv", 1039 | "riproduci serie tv", 1040 | "riproduci telefilm", 1041 | "riproduci serie", 1042 | "riproduci il", 1043 | "riproduci la", 1044 | "riproduci", 1045 | ], 1046 | 1047 | "ordinals": { 1048 | "primo": "1", 1049 | "secondo": "2", 1050 | "terzo": "3", 1051 | "quarto": "4", 1052 | "quinto": "5", 1053 | "sesto": "6", 1054 | "settimo": "7", 1055 | "ottavo": "8", 1056 | "nono": "9", 1057 | "decimo": "10", 1058 | "pre": [ 1059 | "the", 1060 | ], 1061 | "post": [], 1062 | }, 1063 | 1064 | "season": { 1065 | "keywords": [ 1066 | "stagione", 1067 | ], 1068 | "pre": [ 1069 | 'o', 1070 | ], 1071 | "post": [ 1072 | "numero", 1073 | "di", 1074 | ], 1075 | }, 1076 | "episode": { 1077 | "keywords": [ 1078 | "episodi", 1079 | "episodio", 1080 | ], 1081 | "pre": [ 1082 | 'o', 1083 | ], 1084 | "post": [ 1085 | "numero", 1086 | "di", 1087 | ], 1088 | }, 1089 | "latest": { 1090 | "keywords": [ 1091 | "ultimo", 1092 | "recente", 1093 | "nuovo", 1094 | ], 1095 | "pre": [ 1096 | "il", 1097 | "la", 1098 | ], 1099 | "post": [ 1100 | "film", 1101 | "episodi", 1102 | "episodio", 1103 | "serie tv", 1104 | "di", 1105 | ], 1106 | }, 1107 | "unwatched": { 1108 | "keywords": [ 1109 | "non visto", 1110 | "prossimo", 1111 | ], 1112 | "pre": [], 1113 | "post": [ 1114 | "film", 1115 | "episodi", 1116 | "episodio", 1117 | "serie tv", 1118 | "di", 1119 | ], 1120 | }, 1121 | "ondeck": { 1122 | "keywords": [ 1123 | "on deck", 1124 | "ondeck", 1125 | ], 1126 | "pre": [], 1127 | "post": [ 1128 | "film", 1129 | "episodi", 1130 | "episodio", 1131 | "serie tv", 1132 | "di", 1133 | ], 1134 | }, 1135 | "music_album": { 1136 | "keywords": [ 1137 | "album", 1138 | "registrazione", 1139 | ], 1140 | "pre": [], 1141 | "post": [], 1142 | }, 1143 | "music_artist": { 1144 | "keywords": [ 1145 | "artisti", 1146 | "artista", 1147 | "gruppo", 1148 | ], 1149 | "pre": [], 1150 | "post": [], 1151 | }, 1152 | "music_track": { 1153 | "keywords": [ 1154 | "traccia", 1155 | "canzone", 1156 | ], 1157 | "pre": [], 1158 | "post": [], 1159 | }, 1160 | 1161 | "separator": { 1162 | "keywords": [ 1163 | "su", 1164 | ], 1165 | "pre": [], 1166 | "post": [ 1167 | "il", 1168 | ], 1169 | }, 1170 | "music_separator": { 1171 | "keywords": [ 1172 | "di", 1173 | ], 1174 | "pre": [], 1175 | "post": [ 1176 | "il gruppo", 1177 | "gli artisti", 1178 | "l'artista", 1179 | "il gruppo", 1180 | "il", 1181 | ], 1182 | }, 1183 | }, 1184 | "fr": { 1185 | "play": [ 1186 | "joue", 1187 | ], 1188 | "random": [ 1189 | "aléatoire", 1190 | ], 1191 | "movies": [ 1192 | "vidéo", 1193 | "film", 1194 | "films", 1195 | ], 1196 | "shows": [ 1197 | "épisode", 1198 | "l'épisode", 1199 | "tv", 1200 | "télé", 1201 | "série télé", 1202 | "série télévisée", 1203 | "série tv", 1204 | "show", 1205 | "série", 1206 | ], 1207 | "tracks": [], 1208 | "albums": [], 1209 | "artists": [], 1210 | "music": [], 1211 | "playlists": [], 1212 | 1213 | "controls": { 1214 | "play": [ 1215 | "joue", 1216 | ], 1217 | "pause": [ 1218 | "pause", 1219 | ], 1220 | "stop": [ 1221 | "arrête", 1222 | ], 1223 | "next_track": [ 1224 | "suivante", 1225 | ], 1226 | "previous_track": [ 1227 | "précédente", 1228 | ], 1229 | "jump_forward": [ 1230 | "avance", 1231 | ], 1232 | "jump_back": [ 1233 | "recule", 1234 | ], 1235 | }, 1236 | "not_found": "je n'ai pas trouvé", 1237 | "cast_device": "sur", 1238 | "no_call": "aucune commande reçue.", 1239 | "play_start": [ 1240 | "joue le film", 1241 | "joue film", 1242 | "joue la vidéo", 1243 | "joue vidéo", 1244 | "joue la série", 1245 | "joue série", 1246 | "joue le", 1247 | "joue", 1248 | "lire le film", 1249 | "lire film", 1250 | "lis le film", 1251 | "lecture du film", 1252 | "lecture film", 1253 | "lancer série", 1254 | "lancer la série", 1255 | "lance la série", 1256 | "lis la série", 1257 | "lire la série", 1258 | "lire la série télé", 1259 | "lire la série tv", 1260 | "lire la série télévisée", 1261 | "lire le show", 1262 | "lire le show tv", 1263 | "lire télé", 1264 | "lecture de la série", 1265 | "lecture tv", 1266 | "lire tv", 1267 | "lis tv", 1268 | "lance tv", 1269 | "lancer tv", 1270 | "lis série", 1271 | "lire show", 1272 | "lire the", 1273 | "lire", 1274 | ], 1275 | 1276 | "ordinals": { 1277 | "premier": "1", 1278 | "deuxième": "2", 1279 | "troisième": "3", 1280 | "quatrième": "4", 1281 | "cinquième": "5", 1282 | "sixième": "6", 1283 | "septième": "7", 1284 | "huitième": "8", 1285 | "neuvième": "9", 1286 | "dixième": "10", 1287 | "onzième": "11", 1288 | "douzième": "12", 1289 | "treizième": "13", 1290 | "quatorzième": "14", 1291 | "quinzième": "15", 1292 | "pre": [ 1293 | "le", 1294 | ], 1295 | "post": [], 1296 | }, 1297 | 1298 | "season": { 1299 | "keywords": [ 1300 | "saison", 1301 | ], 1302 | "pre": [ 1303 | "la", 1304 | ], 1305 | "post": [ 1306 | "number", 1307 | "de", 1308 | ], 1309 | }, 1310 | "episode": { 1311 | "keywords": [ 1312 | "épisodes", 1313 | "épisode", 1314 | ], 1315 | "pre": [ 1316 | "le", 1317 | "l'", 1318 | ], 1319 | "post": [ 1320 | "nombre", 1321 | "number", 1322 | "de", 1323 | ], 1324 | }, 1325 | "latest": { 1326 | "keywords": [ 1327 | "dernier", 1328 | "récent", 1329 | "nouvel", 1330 | "nouveau", 1331 | ], 1332 | "pre": [ 1333 | "le", 1334 | "un", 1335 | ], 1336 | "post": [ 1337 | "film", 1338 | "vidéo", 1339 | "films", 1340 | "épisodes", 1341 | "épisode", 1342 | "séries", 1343 | "série", 1344 | "tv", 1345 | "télé", 1346 | "télévisée", 1347 | "de la", 1348 | "de", 1349 | ], 1350 | }, 1351 | "unwatched": { 1352 | "keywords": [ 1353 | "non vu", 1354 | "suivant", 1355 | "à avoir", 1356 | "pas encore vu", 1357 | "non-vu", 1358 | ], 1359 | "pre": [ 1360 | "l'", 1361 | "un", 1362 | ], 1363 | "post": [ 1364 | "film", 1365 | "vidéo", 1366 | "épisodes", 1367 | "épisode", 1368 | "films", 1369 | "séries", 1370 | "série", 1371 | "tv", 1372 | "télé", 1373 | "télévisée", 1374 | "de la", 1375 | "de", 1376 | ], 1377 | }, 1378 | "ondeck": { 1379 | "keywords": [ 1380 | "dans pont", 1381 | "dans le pont", 1382 | "sur le pont", 1383 | "pont", 1384 | "deck", 1385 | "récents", 1386 | "favoris", 1387 | "à voir", 1388 | "on deck", 1389 | "ondeck", 1390 | ], 1391 | "pre": [ 1392 | "de mes", 1393 | "dans mon", 1394 | "dans mes", 1395 | "mes", 1396 | "sur", 1397 | "sur le", 1398 | "le", 1399 | "de", 1400 | "dans", 1401 | "mon", 1402 | "mes", 1403 | "sur", 1404 | "le", 1405 | ], 1406 | "post": [ 1407 | "film", 1408 | "vidéo", 1409 | "épisodes", 1410 | "épisode", 1411 | "films", 1412 | "séries", 1413 | "série", 1414 | "tv", 1415 | "télé", 1416 | "télévisée", 1417 | "du", 1418 | "de", 1419 | ], 1420 | }, 1421 | "music_album": { 1422 | "keywords": [], 1423 | "pre": [], 1424 | "post": [], 1425 | }, 1426 | "music_artist": { 1427 | "keywords": [], 1428 | "pre": [], 1429 | "post": [], 1430 | }, 1431 | "music_track": { 1432 | "keywords": [], 1433 | "pre": [], 1434 | "post": [], 1435 | }, 1436 | 1437 | "separator": { 1438 | "keywords": [ 1439 | "sur", 1440 | ], 1441 | "pre": [], 1442 | "post": [ 1443 | "le", 1444 | "la", 1445 | ], 1446 | }, 1447 | "music_separator": { 1448 | "keywords": [], 1449 | "pre": [], 1450 | "post": [], 1451 | }, 1452 | }, 1453 | "pt": { 1454 | "play": [ 1455 | "ver", 1456 | "play", 1457 | ], 1458 | "random": [ 1459 | "aleatório", 1460 | ], 1461 | "movies": [ 1462 | "filmes", 1463 | ], 1464 | "shows": [ 1465 | "episódio", 1466 | "episódios", 1467 | "serie", 1468 | "series", 1469 | ], 1470 | "tracks": [ 1471 | "músicas", 1472 | ], 1473 | "albums": [ 1474 | "albuns", 1475 | ], 1476 | "artists": [ 1477 | "artistas", 1478 | ], 1479 | "music": [ 1480 | "música", 1481 | ], 1482 | "playlists": [ 1483 | "playlists", 1484 | ], 1485 | 1486 | "controls": { 1487 | "play": [ 1488 | "play", 1489 | ], 1490 | "pause": [ 1491 | "pausa", 1492 | ], 1493 | "stop": [ 1494 | "stop", 1495 | ], 1496 | "next_track": [ 1497 | "próximo", 1498 | ], 1499 | "previous_track": [ 1500 | "anterior", 1501 | ], 1502 | "jump_forward": [ 1503 | "para a frente", 1504 | ], 1505 | "jump_back": [ 1506 | "para trás", 1507 | ], 1508 | }, 1509 | 1510 | "not_found": "não encontrado", 1511 | "cast_device": "dispositivo cast", 1512 | "no_call": "Nenhum comando recebido.", 1513 | 1514 | "play_start": [ 1515 | "inicia o filme", 1516 | "começa o filme", 1517 | "inicia a serie", 1518 | "começa a serie", 1519 | "inicia filme", 1520 | "inicia a serie", 1521 | "começar o filme", 1522 | "começar a serie", 1523 | "começa o filme", 1524 | "começa a serie", 1525 | ], 1526 | 1527 | "ordinals": { 1528 | "primeiro": "1", 1529 | "segundo": "2", 1530 | "terceiro": "3", 1531 | "quarto": "4", 1532 | "quinto": "5", 1533 | "sexto": "6", 1534 | "sétimo": "7", 1535 | "oitavo": "8", 1536 | "nono": "9", 1537 | "décimo": "10", 1538 | "pre": [ 1539 | "o", 1540 | "a", 1541 | ], 1542 | "post": [], 1543 | }, 1544 | 1545 | "season": { 1546 | "keywords": [ 1547 | "temporada", 1548 | ], 1549 | "pre": [ 1550 | "de", 1551 | "da", 1552 | "do", 1553 | ], 1554 | "post": [ 1555 | "de", 1556 | "da", 1557 | "do", 1558 | ], 1559 | }, 1560 | "episode": { 1561 | "keywords": [ 1562 | "episódio", 1563 | "episódios", 1564 | ], 1565 | "pre": [ 1566 | "de", 1567 | "da", 1568 | "do", 1569 | ], 1570 | "post": [ 1571 | "número", 1572 | "de", 1573 | "da", 1574 | "do", 1575 | ], 1576 | }, 1577 | "latest": { 1578 | "keywords": [ 1579 | "último", 1580 | "recente", 1581 | "novo", 1582 | ], 1583 | "pre": [ 1584 | "o", 1585 | "a", 1586 | ], 1587 | "post": [ 1588 | "filmes", 1589 | "filme", 1590 | "episódios", 1591 | "episódio", 1592 | "tv", 1593 | "series", 1594 | "serie", 1595 | "de", 1596 | ], 1597 | }, 1598 | "unwatched": { 1599 | "keywords": [ 1600 | "não vistos", 1601 | "próximo", 1602 | ], 1603 | "pre": [ 1604 | "de", 1605 | "da", 1606 | "do", 1607 | ], 1608 | "post": [ 1609 | "filmes", 1610 | "filme", 1611 | "episódios", 1612 | "episódio", 1613 | "tv", 1614 | "series", 1615 | "serie", 1616 | "de", 1617 | ], 1618 | }, 1619 | "ondeck": { 1620 | "keywords": [ 1621 | "ao vivo", 1622 | "live", 1623 | ], 1624 | "pre": [ 1625 | "de", 1626 | "da", 1627 | "do", 1628 | ], 1629 | "post": [ 1630 | "filmes", 1631 | "filme", 1632 | "episódios", 1633 | "episódio", 1634 | "tv", 1635 | "series", 1636 | "serie", 1637 | "de", 1638 | ], 1639 | }, 1640 | "music_album": { 1641 | "keywords": [ 1642 | "album", 1643 | "disco", 1644 | ], 1645 | "pre": [ 1646 | "de", 1647 | "da", 1648 | "do", 1649 | ], 1650 | "post": [ 1651 | "de", 1652 | "da", 1653 | "do", 1654 | ], 1655 | }, 1656 | "music_artist": { 1657 | "keywords": [ 1658 | "artista", 1659 | "artistas", 1660 | "banda", 1661 | "bandas", 1662 | "músico", 1663 | ], 1664 | "pre": [ 1665 | "de", 1666 | "da", 1667 | "do", 1668 | ], 1669 | "post": [ 1670 | "de", 1671 | "da", 1672 | "do", 1673 | ], 1674 | }, 1675 | "music_track": { 1676 | "keywords": [ 1677 | "música", 1678 | ], 1679 | "pre": [ 1680 | "de", 1681 | "da", 1682 | "do", 1683 | ], 1684 | "post": [ 1685 | "de", 1686 | "da", 1687 | "do", 1688 | ], 1689 | }, 1690 | 1691 | "separator": { 1692 | "keywords": [ 1693 | "na", 1694 | ], 1695 | "pre": [ 1696 | "de", 1697 | "da", 1698 | "do", 1699 | ], 1700 | "post": [ 1701 | "de", 1702 | "da", 1703 | "do", 1704 | ], 1705 | }, 1706 | "music_separator": { 1707 | "keywords": [ 1708 | "pelo", 1709 | "pela", 1710 | ], 1711 | "pre": [], 1712 | "post": [ 1713 | "a banda", 1714 | "o músico", 1715 | "o artista", 1716 | "a artista", 1717 | ], 1718 | }, 1719 | }, 1720 | "de": { 1721 | "play": [ 1722 | "spiele", 1723 | ], 1724 | "random": [ 1725 | "zufällige", 1726 | "zufälliges", 1727 | "zufälligen", 1728 | ], 1729 | "movies": [ 1730 | "film", 1731 | ], 1732 | "shows": [ 1733 | "serie", 1734 | "episode", 1735 | "show", 1736 | ], 1737 | "tracks": [], 1738 | "albums": [], 1739 | "artists": [], 1740 | "music": [], 1741 | "playlists": [], 1742 | 1743 | "controls": { 1744 | "play": [ 1745 | "spiele", 1746 | ], 1747 | "pause": [ 1748 | "pausiere", 1749 | ], 1750 | "stop": [ 1751 | "stoppe", 1752 | ], 1753 | "next_track": [ 1754 | "nächstes", 1755 | "nächste", 1756 | "nächsten", 1757 | ], 1758 | "previous_track": [ 1759 | "vorheriges", 1760 | "vorherige", 1761 | "vorherigen", 1762 | ], 1763 | "jump_forward": [ 1764 | "springe vor", 1765 | ], 1766 | "jump_back": [ 1767 | "springe zurück", 1768 | ], 1769 | }, 1770 | "not_found": "nicht gefunden", 1771 | "cast_device": "cast gerät", 1772 | "no_call": "es wurde kein befehl empfangen", 1773 | 1774 | "play_start": [ 1775 | "spiele die show", 1776 | "spiele den film", 1777 | "spiele film", 1778 | "spiele die serie", 1779 | "spiele die", 1780 | "spiele", 1781 | ], 1782 | 1783 | "ordinals": { 1784 | "erste": "1", 1785 | "zweite": "2", 1786 | "dritte": "3", 1787 | "vierte": "4", 1788 | "fünfte": "5", 1789 | "sechste": "6", 1790 | "siebte": "7", 1791 | "achte": "8", 1792 | "neunte": "9", 1793 | "zehnte": "10", 1794 | 1795 | "pre": [ 1796 | "die", 1797 | ], 1798 | "post": [], 1799 | }, 1800 | 1801 | "season": { 1802 | "keywords": [ 1803 | "staffel", 1804 | ], 1805 | "pre": [ 1806 | "ste", 1807 | "te", 1808 | "die", 1809 | ], 1810 | "post": [ 1811 | "von", 1812 | ], 1813 | }, 1814 | "episode": { 1815 | "keywords": [ 1816 | "episode", 1817 | "folge", 1818 | ], 1819 | "pre": [ 1820 | "ste", 1821 | "te", 1822 | "die", 1823 | ], 1824 | "post": [ 1825 | "von", 1826 | ], 1827 | }, 1828 | "latest": { 1829 | "keywords": [ 1830 | "neuste", 1831 | "aktuellste", 1832 | "letzte", 1833 | "aktuelle", 1834 | ], 1835 | "pre": [ 1836 | "die", 1837 | ], 1838 | "post": [ 1839 | "episode", 1840 | "folge", 1841 | "staffel", 1842 | "von", 1843 | ], 1844 | }, 1845 | "unwatched": { 1846 | "keywords": [ 1847 | "nicht gesehenen", 1848 | "nicht gesehene", 1849 | "nächste", 1850 | "folgende", 1851 | ], 1852 | "pre": [ 1853 | "die", 1854 | ], 1855 | "post": [ 1856 | "episode", 1857 | "folge", 1858 | "staffel", 1859 | "von", 1860 | ], 1861 | }, 1862 | "ondeck": { 1863 | "keywords": [ 1864 | "startseite", 1865 | "hauptseite", 1866 | ], 1867 | "pre": [ 1868 | "die", 1869 | ], 1870 | "post": [ 1871 | "von", 1872 | ], 1873 | }, 1874 | "music_album": { 1875 | "keywords": [], 1876 | "pre": [], 1877 | "post": [], 1878 | }, 1879 | "music_artist": { 1880 | "keywords": [], 1881 | "pre": [], 1882 | "post": [], 1883 | }, 1884 | "music_track": { 1885 | "keywords": [], 1886 | "pre": [], 1887 | "post": [], 1888 | }, 1889 | 1890 | "separator": { 1891 | "keywords": [ 1892 | "auf", 1893 | ], 1894 | "pre": [], 1895 | "post": [ 1896 | "dem", 1897 | ], 1898 | }, 1899 | "music_separator": { 1900 | "keywords": [], 1901 | "pre": [], 1902 | "post": [], 1903 | }, 1904 | }, 1905 | "da": { 1906 | "play": [ 1907 | "afspil", 1908 | ], 1909 | "random": [ 1910 | "tilfældig", 1911 | ], 1912 | "movies": [ 1913 | "film", 1914 | ], 1915 | "shows": [ 1916 | "episode", 1917 | "afsnit", 1918 | "tv", 1919 | "program", 1920 | ], 1921 | "tracks": [], 1922 | "albums": [], 1923 | "artists": [], 1924 | "music": [], 1925 | "playlists": [], 1926 | 1927 | "controls": { 1928 | "play": [ 1929 | "afspil", 1930 | ], 1931 | "pause": [ 1932 | "pause", 1933 | ], 1934 | "stop": [ 1935 | "stop", 1936 | ], 1937 | "next_track": [ 1938 | "næste", 1939 | ], 1940 | "previous_track": [ 1941 | "forrige", 1942 | ], 1943 | "jump_forward": [ 1944 | "hop fremad", 1945 | ], 1946 | "jump_back": [ 1947 | "hop tilbage", 1948 | ], 1949 | }, 1950 | 1951 | "not_found": "ikke fundet", 1952 | "cast_device": "cast enhed", 1953 | "no_call": "Ingen kommando modtaget.", 1954 | 1955 | "play_start": [ 1956 | "afspil filmen", 1957 | "afspil film", 1958 | "afspil tv-showet", 1959 | "afspil tv", 1960 | "afspil episoden", 1961 | "afspil tv", 1962 | "afspil afsnittet", 1963 | "afspil tvprogram", 1964 | "afspil afsnit", 1965 | "afspil", 1966 | ], 1967 | 1968 | "ordinals": { 1969 | "første": "1", 1970 | "anden": "2", 1971 | "tredje": "3", 1972 | "fjerde": "4", 1973 | "femte": "5", 1974 | "sjette": "6", 1975 | "syvende": "7", 1976 | "ottende": "8", 1977 | "niende": "9", 1978 | "tiende": "10", 1979 | "ellevte": "11", 1980 | "tolvte": "12", 1981 | "trettende": "13", 1982 | "fjortende": "14", 1983 | "femtende": "15", 1984 | "sekstende": "16", 1985 | "syttende": "17", 1986 | "attende": "18", 1987 | "nittende": "19", 1988 | "tyvende": "20", 1989 | "pre": [ 1990 | "den", 1991 | "det", 1992 | ], 1993 | "post": [], 1994 | }, 1995 | 1996 | "season": { 1997 | "keywords": [ 1998 | "sæson", 1999 | "sæsonnen", 2000 | ], 2001 | "pre": [ 2002 | 'te', 2003 | 'en', 2004 | 'je', 2005 | 'de', 2006 | ], 2007 | "post": [ 2008 | "nummer", 2009 | "af", 2010 | ], 2011 | }, 2012 | "episode": { 2013 | "keywords": [ 2014 | "episode", 2015 | "afsnit", 2016 | "afsnittene", 2017 | "afsnittet", 2018 | "episoder", 2019 | ], 2020 | "pre": [ 2021 | 'te', 2022 | 'en', 2023 | 'je', 2024 | 'de', 2025 | ], 2026 | "post": [ 2027 | "nummer", 2028 | "af", 2029 | ], 2030 | }, 2031 | "latest": { 2032 | "keywords": [ 2033 | "seneste", 2034 | "sidste", 2035 | "nyeste", 2036 | "aktuelle", 2037 | "nye", 2038 | ], 2039 | "pre": [ 2040 | "den", 2041 | ], 2042 | "post": [ 2043 | "film", 2044 | "filmen", 2045 | "episoder", 2046 | "episoden", 2047 | "episoden", 2048 | "tv", 2049 | "afsnit", 2050 | "afsnittet", 2051 | "af", 2052 | ], 2053 | }, 2054 | "unwatched": { 2055 | "keywords": [ 2056 | "usete", 2057 | "efterfølgende", 2058 | "næste", 2059 | ], 2060 | "pre": [], 2061 | "post": [ 2062 | "film", 2063 | "filmen", 2064 | "episode", 2065 | "episoder", 2066 | "episoden", 2067 | "tv", 2068 | "afsnit", 2069 | "afsnittet", 2070 | "af", 2071 | ], 2072 | }, 2073 | "ondeck": { 2074 | "keywords": [ 2075 | "igangværende", 2076 | "fortsat", 2077 | "on deck", 2078 | "ondeck", 2079 | ], 2080 | "pre": [ 2081 | "fortsat", 2082 | ], 2083 | "post": [ 2084 | "film", 2085 | "filmen", 2086 | "episoder", 2087 | "episoden", 2088 | "tv", 2089 | "afsnit", 2090 | "afsnittet", 2091 | "af", 2092 | ], 2093 | }, 2094 | "music_album": { 2095 | "keywords": [], 2096 | "pre": [], 2097 | "post": [], 2098 | }, 2099 | "music_artist": { 2100 | "keywords": [], 2101 | "pre": [], 2102 | "post": [], 2103 | }, 2104 | "music_track": { 2105 | "keywords": [], 2106 | "pre": [], 2107 | "post": [], 2108 | }, 2109 | 2110 | "separator": { 2111 | "keywords": [ 2112 | "på", 2113 | "via", 2114 | "med", 2115 | ], 2116 | "pre": [], 2117 | "post": [], 2118 | }, 2119 | "music_separator": { 2120 | "keywords": [], 2121 | "pre": [], 2122 | "post": [], 2123 | }, 2124 | }, 2125 | "nb": { 2126 | "play": [ 2127 | "spille", 2128 | ], 2129 | "random": [ 2130 | "tilfeldig", 2131 | ], 2132 | "movies": [ 2133 | "filmer", 2134 | "film", 2135 | ], 2136 | "shows": [ 2137 | "avsnitt", 2138 | "tv program", 2139 | "tv", 2140 | "serien", 2141 | "serie", 2142 | ], 2143 | "tracks": [], 2144 | "albums": [], 2145 | "artists": [], 2146 | "music": [], 2147 | "playlists": [], 2148 | 2149 | "controls": { 2150 | "play": [ 2151 | "spille", 2152 | ], 2153 | "pause": [ 2154 | "pause", 2155 | ], 2156 | "stop": [ 2157 | "stopp", 2158 | ], 2159 | "next_track": [ 2160 | "neste", 2161 | ], 2162 | "previous_track": [ 2163 | "forrige", 2164 | ], 2165 | "jump_forward": [ 2166 | "hopp fremover", 2167 | ], 2168 | "jump_back": [ 2169 | "hopp tilbake", 2170 | ], 2171 | }, 2172 | 2173 | "not_found": "finnes ikke", 2174 | "cast_device": "cast enhet", 2175 | "no_call": "Ingen kommandoer mottatt.", 2176 | 2177 | "play_start": [ 2178 | "spill filmen", 2179 | "spill film", 2180 | "spill tv programmet", 2181 | "spill serien", 2182 | "spill serie", 2183 | "spill tv", 2184 | "spill", 2185 | ], 2186 | 2187 | "ordinals": { 2188 | "første": "1", 2189 | "andre": "2", 2190 | "tredje": "3", 2191 | "fjerde": "4", 2192 | "femte": "5", 2193 | "sjette": "6", 2194 | "syvende": "7", 2195 | "åttende": "8", 2196 | "niende": "9", 2197 | "tiende": "10", 2198 | "pre": [], 2199 | "post": [], 2200 | }, 2201 | 2202 | "season": { 2203 | "keywords": [ 2204 | "sesong", 2205 | ], 2206 | "pre": [ 2207 | "a", 2208 | "e", 2209 | ], 2210 | "post": [ 2211 | "nummer", 2212 | "av", 2213 | ], 2214 | }, 2215 | "episode": { 2216 | "keywords": [ 2217 | "episoder", 2218 | "episode", 2219 | ], 2220 | "pre": [ 2221 | "a", 2222 | "e", 2223 | ], 2224 | "post": [ 2225 | "nummer", 2226 | "av", 2227 | ], 2228 | }, 2229 | "latest": { 2230 | "keywords": [ 2231 | "siste", 2232 | "nyligt", 2233 | "ny", 2234 | ], 2235 | "pre": [ 2236 | "det", 2237 | ], 2238 | "post": [ 2239 | "avsnitt", 2240 | "av", 2241 | ], 2242 | }, 2243 | "unwatched": { 2244 | "keywords": [ 2245 | "usitt", 2246 | ], 2247 | "pre": [], 2248 | "post": [ 2249 | "avsnitt", 2250 | "av", 2251 | ], 2252 | }, 2253 | "ondeck": { 2254 | "keywords": [ 2255 | "kommende", 2256 | ], 2257 | "pre": [], 2258 | "post": [ 2259 | "filmer", 2260 | "film", 2261 | "TV program", 2262 | "program", 2263 | "tv", 2264 | ], 2265 | }, 2266 | "music_album": { 2267 | "keywords": [], 2268 | "pre": [], 2269 | "post": [], 2270 | }, 2271 | "music_artist": { 2272 | "keywords": [], 2273 | "pre": [], 2274 | "post": [], 2275 | }, 2276 | "music_track": { 2277 | "keywords": [], 2278 | "pre": [], 2279 | "post": [], 2280 | }, 2281 | 2282 | "separator": { 2283 | "keywords": [ 2284 | "på", 2285 | ], 2286 | "pre": [], 2287 | "post": [], 2288 | }, 2289 | "music_separator": { 2290 | "keywords": [], 2291 | "pre": [], 2292 | "post": [], 2293 | }, 2294 | }, 2295 | "es": { 2296 | "play": [ 2297 | "reproducir", 2298 | ], 2299 | "random": [ 2300 | "aleatorio", 2301 | ], 2302 | "movies": [ 2303 | "película", 2304 | "películas", 2305 | "video", 2306 | "videos", 2307 | "vídeo", 2308 | "vídeos", 2309 | ], 2310 | "shows": [ 2311 | "programa", 2312 | "programas", 2313 | "episodio", 2314 | "episodios", 2315 | "serie", 2316 | "series", 2317 | "serie de televisión", 2318 | "series de televisión", 2319 | ], 2320 | "tracks": [], 2321 | "albums": [], 2322 | "artists": [], 2323 | "music": [], 2324 | "playlists": [], 2325 | 2326 | "controls": { 2327 | "play": [ 2328 | "reproducir", 2329 | ], 2330 | "pause": [ 2331 | "pausa", 2332 | ], 2333 | "stop": [ 2334 | "detener", 2335 | ], 2336 | "next_track": [ 2337 | "siguiente", 2338 | ], 2339 | "previous_track": [ 2340 | "anterior", 2341 | ], 2342 | "jump_forward": [ 2343 | "avanzar", 2344 | ], 2345 | "jump_back": [ 2346 | "retroceder", 2347 | ], 2348 | }, 2349 | 2350 | "not_found": "no encontrado", 2351 | "cast_device": "dispositivo cast", 2352 | "no_call": "ningún comando recibido", 2353 | 2354 | "play_start": [ 2355 | "empieza la película", 2356 | "comienza la película", 2357 | "inicia la película", 2358 | "empieza la serie", 2359 | "comienza la serie", 2360 | "inicia la serie", 2361 | "empieza la serie de televisión", 2362 | "comienza la serie de televisión", 2363 | "inicia la serie de televisión", 2364 | "empieza el episodio", 2365 | "comienza el episodio", 2366 | "inicia el episodio", 2367 | "empieza el programa", 2368 | "comienza el programa", 2369 | "inicia el programa", 2370 | "empieza el video", 2371 | "comienza el video", 2372 | "inicia el video", 2373 | "empieza el vídeo", 2374 | "comienza el vídeo", 2375 | "inicia el vídeo", 2376 | "comienza", 2377 | "comienze", 2378 | "empieza", 2379 | "empiece", 2380 | "inicia", 2381 | "inicie", 2382 | ], 2383 | 2384 | "ordinals": { 2385 | "primero": "1", 2386 | "segundo": "2", 2387 | "tercero": "3", 2388 | "cuarto": "4", 2389 | "quinto": "5", 2390 | "sexto": "6", 2391 | "séptimo": "7", 2392 | "octavo": "8", 2393 | "noveno": "9", 2394 | "décimo": "10", 2395 | "pre": [ 2396 | "el", 2397 | "la", 2398 | ], 2399 | "post": [], 2400 | }, 2401 | 2402 | "season": { 2403 | "keywords": [ 2404 | "temporada", 2405 | ], 2406 | "pre": [ 2407 | "la", 2408 | ], 2409 | "post": [ 2410 | "numero", 2411 | "de", 2412 | ], 2413 | }, 2414 | "episode": { 2415 | "keywords": [ 2416 | "episodio", 2417 | "episodios", 2418 | ], 2419 | "pre": [ 2420 | "el", 2421 | "los", 2422 | ], 2423 | "post": [ 2424 | "número", 2425 | "de", 2426 | ], 2427 | }, 2428 | "latest": { 2429 | "keywords": [ 2430 | "último", 2431 | "última", 2432 | "últimos", 2433 | "últimas", 2434 | "reciente", 2435 | "nuevo", 2436 | "nueva", 2437 | "nuevos", 2438 | "nuevas", 2439 | ], 2440 | "pre": [ 2441 | "el", 2442 | "los", 2443 | "la", 2444 | "las", 2445 | ], 2446 | "post": [ 2447 | "película", 2448 | "películas", 2449 | "episodio", 2450 | "episodios", 2451 | "serie", 2452 | "series", 2453 | "video", 2454 | "vídeo", 2455 | "videos", 2456 | "vídeos", 2457 | ], 2458 | }, 2459 | "unwatched": { 2460 | "keywords": [ 2461 | "no visto", 2462 | "siguiente", 2463 | "aún no visto", 2464 | ], 2465 | "pre": [ 2466 | "película", 2467 | "películas", 2468 | "serie", 2469 | "series", 2470 | "video", 2471 | "vídeo", 2472 | "episodio", 2473 | "episodios", 2474 | ], 2475 | "post": [], 2476 | }, 2477 | "ondeck": { 2478 | "keywords": [ 2479 | "en progreso", 2480 | ], 2481 | "pre": [ 2482 | "película", 2483 | "películas", 2484 | "serie", 2485 | "series", 2486 | "video", 2487 | "vídeo", 2488 | "episodio", 2489 | "episodios", 2490 | ], 2491 | "post": [], 2492 | }, 2493 | "music_album": { 2494 | "keywords": [], 2495 | "pre": [], 2496 | "post": [], 2497 | }, 2498 | "music_artist": { 2499 | "keywords": [], 2500 | "pre": [], 2501 | "post": [], 2502 | }, 2503 | "music_track": { 2504 | "keywords": [], 2505 | "pre": [], 2506 | "post": [], 2507 | }, 2508 | 2509 | "separator": { 2510 | "keywords": [ 2511 | "en", 2512 | ], 2513 | "pre": [], 2514 | "post": [ 2515 | "la", 2516 | ], 2517 | }, 2518 | "music_separator": { 2519 | "keywords": [], 2520 | "pre": [], 2521 | "post": [], 2522 | }, 2523 | }, 2524 | # "template": { 2525 | # # Generic Terms 2526 | # "play": [], 2527 | # "random": [], 2528 | # "movies": [], 2529 | # "shows": [], 2530 | # "tracks": [], 2531 | # "albums": [], 2532 | # "artists": [], 2533 | # "music": [], 2534 | # "playlists": [], 2535 | 2536 | # # Controls 2537 | # "controls": { 2538 | # "play": [], 2539 | # "pause": [], 2540 | # "stop": [], 2541 | # "next_track": [], 2542 | # "previous_track": [], 2543 | # "jump_forward": [], 2544 | # "jump_back": [], 2545 | # }, 2546 | 2547 | # # Text for errors 2548 | # "not_found": "", 2549 | # "cast_device": "", 2550 | # # no_call error is for when no command was recieved 2551 | # "no_call": "", 2552 | 2553 | # # Invoke Command 2554 | # "play_start": [], 2555 | 2556 | # # Ordinal Numbers to Integers 2557 | # "ordinals": { 2558 | # # Edit the keys for translation, not the integers. 2559 | # "first": "1", 2560 | # "second": "2", 2561 | # "third": "3", 2562 | # "fourth": "4", 2563 | # "fifth": "5", 2564 | # "sixth": "6", 2565 | # "seventh": "7", 2566 | # "eighth": "8", 2567 | # "ninth": "9", 2568 | # "tenth": "10", 2569 | # # Do not edit the keys of pre and post 2570 | # "pre": [], 2571 | # "post": [], 2572 | # }, 2573 | 2574 | # # Keywords, Pre, and Post 2575 | # "season": { 2576 | # "keywords": [], 2577 | # "pre": [], 2578 | # "post": [], 2579 | # }, 2580 | # "episode": { 2581 | # "keywords": [], 2582 | # "pre": [], 2583 | # "post": [], 2584 | # }, 2585 | # "latest": { 2586 | # "keywords": [], 2587 | # "pre": [], 2588 | # "post": [], 2589 | # }, 2590 | # "unwatched": { 2591 | # "keywords": [], 2592 | # "pre": [], 2593 | # "post": [], 2594 | # }, 2595 | # "ondeck": { 2596 | # "keywords": [], 2597 | # "pre": [], 2598 | # "post": [], 2599 | # }, 2600 | # "music_album": { 2601 | # "keywords": [], 2602 | # "pre": [], 2603 | # "post": [], 2604 | # }, 2605 | # "music_artist": { 2606 | # "keywords": [], 2607 | # "pre": [], 2608 | # "post": [], 2609 | # }, 2610 | # "music_track": { 2611 | # "keywords": [], 2612 | # "pre": [], 2613 | # "post": [], 2614 | # }, 2615 | 2616 | # # This is the separator word used at the end of the command 2617 | # # to let us know it is a cast device. 2618 | # # Examples: "Play Coco on Samsung TV" - "Play Coco on the Samsung TV" 2619 | # "separator": { 2620 | # "keywords": [], 2621 | # "pre": [], 2622 | # "post": [], 2623 | # }, 2624 | # # This is the separator word used in a command for music 2625 | # # that narrows down a search by using the artists name. 2626 | # # Examples: "Play album by artist" or "Play track by the artist" 2627 | # "music_separator": { 2628 | # "keywords": [], 2629 | # "pre": [], 2630 | # "post": [], 2631 | # }, 2632 | # }, 2633 | } 2634 | --------------------------------------------------------------------------------