Play a Jellyfin Show Using the Sentence Node
This example demonstrates how to use the Sentence node with Home Assistant's conversation integration to play a show from Jellyfin. The flow utilizes dynamic responses to respond with the selected episode.
You can trigger the flow with commands like:
- "Play a random MASH episode"
- "Play MASH episode"
- "Play MASH season six, episode twelve"
- "Play MASH episode 100"
Flow Configuration
The flow leverages these nodes:
[{"id":"13b4618a99045b71","type":"ha-sentence","z":"d728cb91f51ff055","name":"random MASH episode","server":"2dad33ee.42bf5c","version":2,"inputs":0,"outputs":1,"exposeAsEntityConfig":"","mode":"trigger","sentences":["play [a] [random] mash (episode|show)"],"response":"","responseType":"str","triggerResponse":"Something went wrong","triggerResponseType":"dynamic","responseTimeout":"2000","outputProperties":[{"property":"seriesId","propertyType":"msg","value":"3b264a3b9e602ddc9c9a00a343dd5ce2","valueType":"str"},{"property":"deviceId","propertyType":"msg","value":"c76c8695549b5bedfc21f48b0664b8329ed2ebde","valueType":"str"}],"x":124,"y":144,"wires":[["45d8457d15bfa505"]]},{"id":"41a351cf86e6a70e","type":"random-item","z":"d728cb91f51ff055","name":"","input":"mash_episodes","inputType":"flow","output":"payload","outputType":"msg","number":1,"x":502,"y":144,"wires":[["7de8f9a16179ad63"]]},{"id":"04f10897f711e88c","type":"change","z":"d728cb91f51ff055","name":"set item id","rules":[{"t":"set","p":"_itemId","pt":"msg","to":"payload.id","tot":"msg","dc":true}],"action":"","property":"","from":"","to":"","reg":false,"x":850,"y":144,"wires":[["a29f9d7e6a746a9e"]]},{"id":"a29f9d7e6a746a9e","type":"openApi-red","z":"d728cb91f51ff055","name":"","configUrlNode":"9435086ff9117a76","apiTag":"Session","keepAuth":true,"operationData":{"id":"GetSessions","hasOperationId":true,"path":"/Sessions","method":"get"},"outputStyle":"each response","errorHandling":"throw exception","internalErrors":{"text":"","source":""},"parameters":{"controllableByUserId query":{"name":"controllableByUserId","type":"jsonata","value":"$env(\"userId\")","in":"query"},"deviceId query":{"name":"deviceId","isActive":true,"type":"msg","value":"deviceId","in":"query"},"activeWithinSeconds query":{"name":"activeWithinSeconds","in":"query"}},"requestContentType":"application/json","responseContentType":"application/json","outputs":3,"responseOutputLabels":[{"code":"200","text":"List of sessions returned."},{"code":"401","text":"Unauthorized"},{"code":"403","text":"Forbidden"}],"responseAsPayload":false,"debugMode":"","headers":[],"_version":"2.1.0","x":1010,"y":144,"wires":[["13987ef9a8e0286f"],[],[]]},{"id":"13987ef9a8e0286f","type":"openApi-red","z":"d728cb91f51ff055","name":"","configUrlNode":"9435086ff9117a76","apiTag":"Session","keepAuth":true,"operationData":{"id":"Play","hasOperationId":true,"path":"/Sessions/{sessionId}/Playing","method":"post"},"outputStyle":"each response","errorHandling":"throw exception","internalErrors":{"text":"","source":""},"parameters":{"itemIds query":{"name":"itemIds","isActive":true,"required":true,"type":"msg","value":"_itemId","in":"query"},"playCommand query":{"name":"playCommand","isActive":true,"required":true,"type":"select","value":"PlayNow","in":"query"},"sessionId path":{"name":"sessionId","isActive":true,"required":true,"type":"msg","value":"payload.0.Id","in":"path"},"startPositionTicks query":{"name":"startPositionTicks","in":"query"},"mediaSourceId query":{"name":"mediaSourceId","in":"query"},"audioStreamIndex query":{"name":"audioStreamIndex","in":"query"},"subtitleStreamIndex query":{"name":"subtitleStreamIndex","in":"query"},"startIndex query":{"name":"startIndex","in":"query"}},"requestContentType":"application/json","responseContentType":"","outputs":3,"responseOutputLabels":[{"code":"204","text":"Instruction sent to session."},{"code":"401","text":"Unauthorized"},{"code":"403","text":"Forbidden"}],"responseAsPayload":false,"debugMode":false,"headers":[],"_version":"2.1.0","x":1146,"y":144,"wires":[["e47af5331f32c92d"],[],[]]},{"id":"b907d1659b1fb6cc","type":"openApi-red","z":"d728cb91f51ff055","name":"","configUrlNode":"9435086ff9117a76","apiTag":"Session","keepAuth":true,"operationData":{"id":"GetSessions","hasOperationId":true,"path":"/Sessions","method":"get"},"outputStyle":"each response","errorHandling":"throw exception","internalErrors":{"text":"","source":""},"parameters":{"controllableByUserId query":{"name":"controllableByUserId","type":"jsonata","value":"$env(\"userId\")","in":"query"},"deviceId query":{"name":"deviceId","type":"msg","value":"deviceId","in":"query"},"activeWithinSeconds query":{"name":"activeWithinSeconds","in":"query"}},"requestContentType":"application/json","responseContentType":"application/json","outputs":3,"responseOutputLabels":[{"code":"200","text":"List of sessions returned."},{"code":"401","text":"Unauthorized"},{"code":"403","text":"Forbidden"}],"responseAsPayload":false,"debugMode":"","headers":[],"_version":"2.1.0","x":302,"y":354,"wires":[["21244d4a0ab99de7"],[],[]]},{"id":"47b0ace1ee8cc84e","type":"inject","z":"d728cb91f51ff055","name":"Find device ID","props":[],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":115,"y":351,"wires":[["b907d1659b1fb6cc"]]},{"id":"21244d4a0ab99de7","type":"debug","z":"d728cb91f51ff055","name":"devices","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload {DeviceName: $.{\"client\": Client, \"deviceId\": DeviceId}}","targetType":"jsonata","statusVal":"","statusType":"auto","x":468,"y":354,"wires":[]},{"id":"e47af5331f32c92d","type":"ha-sentence","z":"d728cb91f51ff055","name":"send response back to HA","server":"2dad33ee.42bf5c","version":2,"inputs":1,"outputs":1,"exposeAsEntityConfig":"","mode":"response","sentences":["play [a] random mash episode"],"response":"_response","responseType":"msg","triggerResponse":"Something went wrong","triggerResponseType":"dynamic","responseTimeout":1000,"outputProperties":[],"x":1050,"y":192,"wires":[[]]},{"id":"7de8f9a16179ad63","type":"template","z":"d728cb91f51ff055","name":"format response","field":"_response","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"Playing MASH {{payload.seasonName}} Episode {{payload.episodeNumber}}, {{{payload.episodeName}}}","output":"str","x":680,"y":144,"wires":[["04f10897f711e88c"]]},{"id":"d050c8425ac14d8b","type":"openApi-red","z":"d728cb91f51ff055","name":"","configUrlNode":"9435086ff9117a76","apiTag":"Search","keepAuth":true,"operationData":{"id":"GetSearchHints","hasOperationId":true,"path":"/Search/Hints","method":"get"},"outputStyle":"each response","errorHandling":"throw exception","internalErrors":{"text":"","source":""},"parameters":{"searchTerm query":{"name":"searchTerm","isActive":true,"required":true,"type":"str","value":"M*A*S*H","in":"query"},"startIndex query":{"name":"startIndex","in":"query"},"limit query":{"name":"limit","in":"query"},"userId query":{"name":"userId","in":"query"},"includeItemTypes query":{"name":"includeItemTypes","in":"query"},"excludeItemTypes query":{"name":"excludeItemTypes","in":"query"},"mediaTypes query":{"name":"mediaTypes","in":"query"},"parentId query":{"name":"parentId","in":"query"},"isMovie query":{"name":"isMovie","in":"query"},"isSeries query":{"name":"isSeries","in":"query"},"isNews query":{"name":"isNews","in":"query"},"isKids query":{"name":"isKids","in":"query"},"isSports query":{"name":"isSports","in":"query"},"includePeople query":{"name":"includePeople","in":"query"},"includeMedia query":{"name":"includeMedia","in":"query"},"includeGenres query":{"name":"includeGenres","in":"query"},"includeStudios query":{"name":"includeStudios","in":"query"},"includeArtists query":{"name":"includeArtists","in":"query"}},"requestContentType":"application/json","responseContentType":"application/json","outputs":3,"responseOutputLabels":[{"code":"200","text":"Search hint returned."},{"code":"401","text":"Unauthorized"},{"code":"403","text":"Forbidden"}],"responseAsPayload":false,"debugMode":"","headers":[],"_version":"2.1.0","x":308,"y":451,"wires":[["63e5998dd3c9dba0"],[],[]]},{"id":"87d85ce4c11270d6","type":"inject","z":"d728cb91f51ff055","name":"Find series ID","props":[],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":111,"y":448,"wires":[["d050c8425ac14d8b"]]},{"id":"63e5998dd3c9dba0","type":"debug","z":"d728cb91f51ff055","name":"results","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload.SearchHints {Name: $.{\"seriesId\": Id, \"series\": Series, \"year\": ProductionYear, \"type\": Type}}","targetType":"jsonata","statusVal":"","statusType":"auto","x":466,"y":451,"wires":[]},{"id":"e286325a4e63d372","type":"openApi-red","z":"d728cb91f51ff055","name":"get series episodes","configUrlNode":"9435086ff9117a76","apiTag":"Items","keepAuth":true,"operationData":{"id":"GetItems","hasOperationId":true,"path":"/Items","method":"get"},"outputStyle":"each response","errorHandling":"throw exception","internalErrors":{"text":"","source":""},"parameters":{"userId query":{"name":"userId","isActive":true,"type":"jsonata","value":"$env(\"userId\")","in":"query"},"maxOfficialRating query":{"name":"maxOfficialRating","in":"query"},"hasThemeSong query":{"name":"hasThemeSong","in":"query"},"hasThemeVideo query":{"name":"hasThemeVideo","in":"query"},"hasSubtitles query":{"name":"hasSubtitles","in":"query"},"hasSpecialFeature query":{"name":"hasSpecialFeature","in":"query"},"hasTrailer query":{"name":"hasTrailer","in":"query"},"adjacentTo query":{"name":"adjacentTo","in":"query"},"parentIndexNumber query":{"name":"parentIndexNumber","in":"query"},"hasParentalRating query":{"name":"hasParentalRating","in":"query"},"isHd query":{"name":"isHd","in":"query"},"is4K query":{"name":"is4K","in":"query"},"locationTypes query":{"name":"locationTypes","in":"query"},"excludeLocationTypes query":{"name":"excludeLocationTypes","in":"query"},"isMissing query":{"name":"isMissing","in":"query"},"isUnaired query":{"name":"isUnaired","in":"query"},"minCommunityRating query":{"name":"minCommunityRating","in":"query"},"minCriticRating query":{"name":"minCriticRating","in":"query"},"minPremiereDate query":{"name":"minPremiereDate","in":"query"},"minDateLastSaved query":{"name":"minDateLastSaved","in":"query"},"minDateLastSavedForUser query":{"name":"minDateLastSavedForUser","in":"query"},"maxPremiereDate query":{"name":"maxPremiereDate","in":"query"},"hasOverview query":{"name":"hasOverview","in":"query"},"hasImdbId query":{"name":"hasImdbId","in":"query"},"hasTmdbId query":{"name":"hasTmdbId","in":"query"},"hasTvdbId query":{"name":"hasTvdbId","in":"query"},"isMovie query":{"name":"isMovie","type":"bool","value":"true","in":"query"},"isSeries query":{"name":"isSeries","in":"query"},"isNews query":{"name":"isNews","in":"query"},"isKids query":{"name":"isKids","in":"query"},"isSports query":{"name":"isSports","in":"query"},"excludeItemIds query":{"name":"excludeItemIds","in":"query"},"startIndex query":{"name":"startIndex","in":"query"},"limit query":{"name":"limit","type":"num","value":"1","in":"query"},"recursive query":{"name":"recursive","isActive":true,"type":"bool","value":"true","in":"query"},"searchTerm query":{"name":"searchTerm","in":"query"},"sortOrder query":{"name":"sortOrder","type":"str","collapsed":false,"value":"Descending","in":"query"},"parentId query":{"name":"parentId","isActive":true,"type":"msg","value":"seriesId","in":"query"},"fields query":{"name":"fields","in":"query"},"excludeItemTypes query":{"name":"excludeItemTypes","in":"query"},"includeItemTypes query":{"name":"includeItemTypes","isActive":true,"type":"str","value":"Episode","in":"query"},"filters query":{"name":"filters","in":"query"},"isFavorite query":{"name":"isFavorite","in":"query"},"mediaTypes query":{"name":"mediaTypes","in":"query"},"imageTypes query":{"name":"imageTypes","in":"query"},"sortBy query":{"name":"sortBy","type":"str","value":"DatePlayed","in":"query"},"isPlayed query":{"name":"isPlayed","in":"query"},"genres query":{"name":"genres","in":"query"},"officialRatings query":{"name":"officialRatings","in":"query"},"tags query":{"name":"tags","in":"query"},"years query":{"name":"years","in":"query"},"enableUserData query":{"name":"enableUserData","in":"query"},"imageTypeLimit query":{"name":"imageTypeLimit","in":"query"},"enableImageTypes query":{"name":"enableImageTypes","in":"query"},"person query":{"name":"person","in":"query"},"personIds query":{"name":"personIds","in":"query"},"personTypes query":{"name":"personTypes","in":"query"},"studios query":{"name":"studios","in":"query"},"artists query":{"name":"artists","in":"query"},"excludeArtistIds query":{"name":"excludeArtistIds","in":"query"},"artistIds query":{"name":"artistIds","in":"query"},"albumArtistIds query":{"name":"albumArtistIds","in":"query"},"contributingArtistIds query":{"name":"contributingArtistIds","in":"query"},"albums query":{"name":"albums","in":"query"},"albumIds query":{"name":"albumIds","in":"query"},"ids query":{"name":"ids","in":"query"},"videoTypes query":{"name":"videoTypes","in":"query"},"minOfficialRating query":{"name":"minOfficialRating","in":"query"},"isLocked query":{"name":"isLocked","in":"query"},"isPlaceHolder query":{"name":"isPlaceHolder","in":"query"},"hasOfficialRating query":{"name":"hasOfficialRating","in":"query"},"collapseBoxSetItems query":{"name":"collapseBoxSetItems","in":"query"},"minWidth query":{"name":"minWidth","in":"query"},"minHeight query":{"name":"minHeight","in":"query"},"maxWidth query":{"name":"maxWidth","in":"query"},"maxHeight query":{"name":"maxHeight","in":"query"},"is3D query":{"name":"is3D","in":"query"},"seriesStatus query":{"name":"seriesStatus","in":"query"},"nameStartsWithOrGreater query":{"name":"nameStartsWithOrGreater","in":"query"},"nameStartsWith query":{"name":"nameStartsWith","in":"query"},"nameLessThan query":{"name":"nameLessThan","in":"query"},"studioIds query":{"name":"studioIds","in":"query"},"genreIds query":{"name":"genreIds","in":"query"},"enableTotalRecordCount query":{"name":"enableTotalRecordCount","in":"query"},"enableImages query":{"name":"enableImages","in":"query"}},"requestContentType":"application/json","responseContentType":"application/json","outputs":3,"responseOutputLabels":[{"code":"200","text":"Success"},{"code":"401","text":"Unauthorized"},{"code":"403","text":"Forbidden"}],"responseAsPayload":false,"debugMode":false,"headers":[],"_version":"2.1.0","x":546,"y":290,"wires":[["57dccb79c1e6b527"],[],[]]},{"id":"57dccb79c1e6b527","type":"function","z":"d728cb91f51ff055","name":"store episodes in flow context","func":"const episodes = flow.get(\"mash_episodes\") ?? [];\n// only keep the data we need\nmsg.payload.Items.forEach((e) => {\n episodes.push({\n id: e.Id,\n episodeNumber: e.IndexNumber,\n seasonName: e.SeasonName,\n episodeName: e.Name,\n }) \n});\nflow.set(\"mash_episodes\", episodes);\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":794,"y":290,"wires":[["b195cd17f0d831f6"]]},{"id":"b195cd17f0d831f6","type":"link out","z":"d728cb91f51ff055","name":"","mode":"return","links":[],"x":973,"y":290,"wires":[]},{"id":"956492fb58071a3f","type":"link in","z":"d728cb91f51ff055","name":"get episodes","links":[],"x":94,"y":290,"wires":[["ae47bac31c4abf22"]],"l":true},{"id":"45d8457d15bfa505","type":"link call","z":"d728cb91f51ff055","name":"get episodes","links":["956492fb58071a3f"],"linkType":"static","timeout":"30","x":330,"y":144,"wires":[["41a351cf86e6a70e"]]},{"id":"cf88a7b5365c7c0f","type":"ha-sentence","z":"d728cb91f51ff055","name":"play MASH episode","server":"2dad33ee.42bf5c","version":2,"inputs":0,"outputs":1,"exposeAsEntityConfig":"","mode":"trigger","sentences":["play mash season {season} episode {episode}","play mash episode {episode}"],"response":"","responseType":"str","triggerResponse":"Something went wrong","triggerResponseType":"dynamic","responseTimeout":"2000","outputProperties":[{"property":"payload","propertyType":"msg","value":"","valueType":"results"},{"property":"deviceId","propertyType":"msg","value":"c76c8695549b5bedfc21f48b0664b8329ed2ebde","valueType":"str"},{"property":"seriesId","propertyType":"msg","value":"3b264a3b9e602ddc9c9a00a343dd5ce2","valueType":"str"}],"x":114,"y":192,"wires":[["3936fa1204b55b3f"]]},{"id":"ae47bac31c4abf22","type":"switch","z":"d728cb91f51ff055","name":"have episode list?","property":"mash_episodes","propertyType":"flow","rules":[{"t":"empty"},{"t":"null"},{"t":"else"}],"checkall":"true","repair":false,"outputs":3,"x":326,"y":290,"wires":[["e286325a4e63d372"],["e286325a4e63d372"],["b195cd17f0d831f6"]]},{"id":"a87cff07f2f77174","type":"function","z":"d728cb91f51ff055","name":"find episode","func":"const episodes = flow.get(\"mash_episodes\") ?? [];\nconst episode = convertStringToNumber(msg.payload.entities.episode.value);\nconst season = convertStringToNumber(msg.payload.entities.season?.value);\n\nif (season) {\n msg.payload = episodes.find(e => e.episodeNumber === episode && e.seasonName === `Season ${season}`);\n} else { // if season is empty find show by number is aired in series\n msg.payload = findNthShowAired(episodes, episode);\n}\n\n// episode not found\nif (!msg.payload) {\n msg._response = `No episode found for MASH ${season ? `Season ${season}` : ''} Episode ${episode}`;\n return [null, msg];\n}\nreturn msg;\n\nfunction findNthShowAired(episodes, nthShow) {\n if (nthShow <= 0 || nthShow > episodes.length) {\n return null; // Invalid input or out of range\n }\n\n // Sort episodes by season and episode number to ensure correct airing order\n const sortedEpisodes = episodes.sort((a, b) => {\n const seasonA = Number(a.seasonName.replace('Season ', ''));\n const seasonB = Number(b.seasonName.replace('Season ', ''));\n\n if (seasonA === seasonB) {\n return a.episodeNumber - b.episodeNumber;\n } else {\n return seasonA - seasonB;\n }\n });\n\n return sortedEpisodes[nthShow - 1]; // nthShow is 1-based, array is 0-based\n}\n\nfunction convertStringToNumber(input) {\n if (input === undefined) return;\n \n const numberWords = {\n 'zero': 0, 'one': 1, 'two': 2, 'three': 3, 'four': 4, \n 'five': 5, 'six': 6, 'seven': 7, 'eight': 8, 'nine': 9, \n 'ten': 10, 'eleven': 11, 'twelve': 12, 'thirteen': 13, \n 'fourteen': 14, 'fifteen': 15, 'sixteen': 16, 'seventeen': 17, \n 'eighteen': 18, 'nineteen': 19, 'twenty': 20\n };\n\n // Check if input is already a numeric string\n if (!isNaN(input)) {\n return Number(input);\n }\n\n // Check if the input is a number word\n const lowerInput = input.toLowerCase().trim();\n if (lowerInput in numberWords) {\n return numberWords[lowerInput];\n }\n}","outputs":2,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":502,"y":192,"wires":[["7de8f9a16179ad63"],["e47af5331f32c92d"]]},{"id":"3936fa1204b55b3f","type":"link call","z":"d728cb91f51ff055","name":"get episodes","links":["956492fb58071a3f"],"linkType":"static","timeout":"30","x":330,"y":192,"wires":[["a87cff07f2f77174"]]},{"id":"31e71416c0d8fcd4","type":"comment","z":"d728cb91f51ff055","name":"The deviceId and seriesId will need to be changed in the output properties of the sentence node","info":"","x":354,"y":96,"wires":[]},{"id":"dcc480460a475cf8","type":"openApi-red","z":"d728cb91f51ff055","name":"","configUrlNode":"9435086ff9117a76","apiTag":"User","keepAuth":true,"operationData":{"id":"GetPublicUsers","hasOperationId":true,"path":"/Users/Public","method":"get"},"outputStyle":"each response","errorHandling":"throw exception","internalErrors":{"text":"","source":""},"parameters":{},"requestContentType":"application/json","responseContentType":"application/json","outputs":1,"responseOutputLabels":[{"code":"200","text":"Public users returned."}],"responseAsPayload":false,"debugMode":"","headers":[],"_version":"2.1.0","x":312,"y":513,"wires":[["7bfc313b1590bc35"]]},{"id":"3225bc86df87feba","type":"inject","z":"d728cb91f51ff055","name":"Find user ID","props":[],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":114,"y":513,"wires":[["dcc480460a475cf8"]]},{"id":"7bfc313b1590bc35","type":"debug","z":"d728cb91f51ff055","name":"results","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload {Name: Id}","targetType":"jsonata","statusVal":"","statusType":"auto","x":470,"y":513,"wires":[]},{"id":"db16886a9eee2c17","type":"comment","z":"d728cb91f51ff055","name":"The user ID will need to be changed in the \"get series episodes\"","info":"","x":686,"y":240,"wires":[]},{"id":"35ebe5b08b627b76","type":"comment","z":"d728cb91f51ff055","name":"Edit searchTerm","info":"","x":308,"y":400,"wires":[]}]
Setup Instructions
The flow includes nodes to retrieve the necessary data from Jellyfin. Follow these steps to set it up:
- Install the required nodes.
- Import the provided flow into Node-RED.
- Configure the
openapi
node:- Use the Jellyfin OpenAPI URL:
https://api.jellyfin.org/openapi/jellyfin-openapi-stable.json
. - Enter your Jellyfin server details.
- Add your API key in the custom header under
X-MediaBrowser-Token
.
- Use the Jellyfin OpenAPI URL:
- Adjust the Sentence node to recognize your desired show's name and the season/episode format.
- In the "get series episodes" node, update the
userId
field with your Jellyfin user ID. - Deploy the flow.
Extended Usage
This flow can be expanded to include additional functionality, such as:
- Customizing which devices the show is played on.
- Playing a random episode from a specific show.
- Playing a random episode from a specific season.
- Playing the next unwatched episode from a show.
- Playing the next episode from a show.
- Playing a random episode from a specific genre.