diff --git a/README.md b/README.md index 5133d4e..37a7a4f 100644 --- a/README.md +++ b/README.md @@ -19,9 +19,17 @@ This is an Alexa Skill that can be used to control your personal Audiobookshelf - performs an an API "search" that is built-in to ABS - if this fails, pulls all books from library and then performs a fuzzy search (effective, but may be resource intensive on large libraries) -## Installation: -1) Fork this repo -2) Edit the config.js file to include your **Audiobookshelf API key** and **server URL** (you can do this later in the 'Code' tab of Developer Console if using Alexa-hosted) +## Installation (Alexa hosted): +1) Follow the instructions here: https://developer.amazon.com/en-US/docs/alexa/hosted-skills/alexa-hosted-skills-git-import.html#import +2) Set your skill invocation name +3) Use the "Code" tab to edit config.js to include your **Audiobookshelf API key** and **server URL** +4) (Optional) Change locale +5) Save and deploy the skill +6) If using Alexa-hosted, go to the 'Test' tab of Developer Console, and enable skill testing for 'Development' + +## Installation (local hosted): +1) Fork this repo (and mark it private) +2) Edit the config.js file to include your **Audiobookshelf API key** and **server URL** 3) Follow the instructions here: https://developer.amazon.com/en-US/docs/alexa/hosted-skills/alexa-hosted-skills-git-import.html#import 4) Set your skill invocation name and build the skill 5) Save and deploy the skill @@ -61,3 +69,10 @@ This is an Alexa Skill that can be used to control your personal Audiobookshelf - [ ] Add other intents, such as: - [ ] "Start the book over" - [ ] "Go to chapter 12" + +## Language Contribution: +- Copy /skill-package/interactionModels/custom/en-US.json and edit the samples to reflect the locale you want to contribute. +- Edit /skill-package/skill.json to include the new locale under manifest/publishingInformation/locales +- Copy /lambda/locale/en-US.js and edit every string to reflect the new locale. + - If unsure about the usage of strings, open /lambda/index.js and search for ".speak" to find every time alexa is told to speak something. + - Maybe the new locale needs restructuring of the string construction - if you edit those, be sure to reflect the changes in all files under /lambda/locales/ diff --git a/lambda/config.js b/lambda/config.js index 4762430..92719d4 100644 --- a/lambda/config.js +++ b/lambda/config.js @@ -1,9 +1,10 @@ // config.js module.exports = { + localeconf: 'en-US', // may also be any other locale as defined in the folder locale and the package.json - use the locale your alexa is located in ABS_API_KEY: 'xxxxxx', SERVER_URL: 'https://abs.domain.tld', USER_AGENT: 'AlexaSkill' // OTHER OPTIONAL HEADERS // CFAccessClientId: 'xxxxxxxxx.access', // CFAccessClientSecret: 'xxxxxxxx' -}; \ No newline at end of file +}; diff --git a/lambda/index.js b/lambda/index.js index d4a6447..cddc580 100644 --- a/lambda/index.js +++ b/lambda/index.js @@ -21,8 +21,27 @@ let localSessionAttributes = { //const ddbAdapter = require('ask-sdk-dynamodb-persistence-adapter'); // const Util = require('./util.js'); -const { ABS_API_KEY, SERVER_URL, CFAccessClientId, CFAccessClientSecret } = require('./config.js'); +const { localeconf, ABS_API_KEY, SERVER_URL, CFAccessClientId, CFAccessClientSecret } = require('./config.js'); // const { off, title } = require('process'); +const localefile = './locale/' + localeconf + '.js' +const { + SpeakWelcome, + SpeakPlaying, + SpeakBy, + SpeakTryAgain, + SpeakPlaybackBookHandlerNoBookTitle, + SpeakPlaybackBookHandlerNoBookFoundP1, + SpeakPlaybackBookHandlerNoBookFoundP2, + SpeakPlayBookIntentHandlerNoBookTitle, + SpeakPlayBookIntentHandlerNoBookFound, + SpeakUnsupportedAudioIntentHandler, + SpeakHelpIntentHandler, + SpeakCancelAndStopIntentHandlerGoodbye, + SpeakFallbackIntentHandler, + SpeakIntentReflectorHandlerP1, + SpeakIntentReflectorHandlerP2, + SpeakErrorHandler +} = require(localefile); const baseheaders = { "Content-Type": 'application/json', @@ -413,7 +432,7 @@ const LaunchRequestHandler = { return Alexa.getRequestType(handlerInput.requestEnvelope) === 'LaunchRequest'; }, handle(handlerInput) { - const speakOutput = 'Welcome to Audiobookshelf, you can say "play audiobook" to start listening.'; + const speakOutput = SpeakWelcome; console.log(" ~~~ LOGGED AT END OF LaunchRequestHandler ") @@ -571,7 +590,7 @@ const PlayAudioIntentHandler = { //speakOutput = 'Resuming...'; } else { - speakOutput = 'Playing ' + bookTitle + " by " + author; + speakOutput = SpeakPlaying + bookTitle + " " + SpeakBy + " " + author; } console.log("Playing: " + playUrl) @@ -747,7 +766,7 @@ const PlaybackBookHandler = { // this handler is not currently used (has limitat console.log("Title: " + bookTitle) console.log("Author: " + author) if (!bookTitle) { - let speakOutput = 'I did not understand the request. For example, try saying "Play audiobook title by author".'; + let speakOutput = SpeakPlaybackBookHandlerNoBookTitle; console.log("Book and/or author slot undefined") return handlerInput.responseBuilder .speak(speakOutput) @@ -803,7 +822,7 @@ const PlaybackBookHandler = { // this handler is not currently used (has limitat }); if (results[0].book.length == 0) { console.log("No book of title '" + bookTitle + "' found") - const speakOutput = "No book of title '" + bookTitle + "' found. Please try again."; + const speakOutput = SpeakPlaybackBookHandlerNoBookFoundP1 + bookTitle + SpeakPlaybackBookHandlerNoBookFoundP2; return handlerInput.responseBuilder .speak(sanitizeForSSML(speakOutput)) .reprompt(sanitizeForSSML(speakOutput)) @@ -870,7 +889,7 @@ const PlaybackBookHandler = { // this handler is not currently used (has limitat // sync localSessionAttributes to sessionAttributes updateLocalSessionAttributes(sessionAttributes) - let speakOutput = 'Playing ' + userPlaySession.displayTitle + ' by ' + userPlaySession.displayAuthor; + let speakOutput = userPlaySession.displayTitle + ' ' + SpeakBy + ' ' + userPlaySession.displayAuthor; console.log("Playing: " + playUrl) const chapterTitle = getCurrentChapterByBookTime(currentTime, userPlaySession).title @@ -1142,7 +1161,7 @@ const PlayBookIntentHandler = { // require a book title (could later implement playing by author I suppose, maybe in another intent) if (!bookTitle) { - let speakOutput = 'I did not understand the request. For example, try saying "Play audiobook title by author".'; + let speakOutput = SpeakPlayBookIntentHandlerNoBookTitle; console.log("Book and/or author slot undefined") return handlerInput.responseBuilder .speak(speakOutput) @@ -1359,13 +1378,13 @@ const PlayBookIntentHandler = { // if an author was found, could offer to play one of their other books instead? // Would probably need to forward it to another intent.. if (libraryItem) { - console.log("Found a book in the library!") + console.log("Found a book in the library!"); console.log("Title: " + libraryItem.media.metadata.title); console.log("Author: " + libraryItem.media.metadata.authorName); } else { console.log("Could not find a playable book (" + rawTitle + " by " + rawAuthor + ")") - let speakOutput = "Could not find a playable book matching: " + rawTitle + " by " + rawAuthor + ". Please try again." + let speakOutput = SpeakPlayBookIntentHandlerNoBookFound + ' ' + rawTitle + " " + SpeakBy + " " + rawAuthor + ". " + SpeakTryAgain; return handlerInput.responseBuilder .speak(sanitizeForSSML(speakOutput)) .reprompt(sanitizeForSSML(speakOutput)) @@ -1428,7 +1447,7 @@ const PlayBookIntentHandler = { // sync localSessionAttributes to sessionAttributes updateLocalSessionAttributes(sessionAttributes) - let speakOutput = 'Playing ' + userPlaySession.displayTitle + ' by ' + userPlaySession.displayAuthor; + let speakOutput = userPlaySession.displayTitle + ' ' + SpeakBy + ' ' + userPlaySession.displayAuthor; console.log("Playing: " + playUrl) @@ -2061,7 +2080,7 @@ const UnsupportedAudioIntentHandler = { ); }, async handle(handlerInput) { - const speakOutput = 'Sorry, I can\'t support that yet.'; + const speakOutput = SpeakUnsupportedAudioIntentHandler; return handlerInput.responseBuilder .speak(sanitizeForSSML(speakOutput)) @@ -2075,7 +2094,7 @@ const HelpIntentHandler = { && Alexa.getIntentName(handlerInput.requestEnvelope) === 'AMAZON.HelpIntent'; }, handle(handlerInput) { - const speakOutput = 'You can say "play audio" to start playing your book! How can I help?'; + const speakOutput = SpeakHelpIntentHandler; return handlerInput.responseBuilder .speak(speakOutput) @@ -2095,7 +2114,7 @@ const CancelAndStopIntentHandler = { || Alexa.getIntentName(handlerInput.requestEnvelope) === 'AMAZON.StopIntent'); }, handle(handlerInput) { - const speakOutput = 'Goodbye!'; + const speakOutput = SpeakCancelAndStopIntentHandlerGoodbye; const sessionAttributes = handlerInput.attributesManager.getSessionAttributes() const userPlaySession = sessionAttributes.userPlaySession @@ -2663,7 +2682,7 @@ const FallbackIntentHandler = { && Alexa.getIntentName(handlerInput.requestEnvelope) === 'AMAZON.FallbackIntent'; }, handle(handlerInput) { - const speakOutput = 'Sorry, I don\'t know about that. Please try again.'; + const speakOutput = SpeakFallbackIntentHandler; return handlerInput.responseBuilder .speak(speakOutput) @@ -2749,7 +2768,7 @@ const IntentReflectorHandler = { }, handle(handlerInput) { const intentName = Alexa.getIntentName(handlerInput.requestEnvelope); - const speakOutput = `You just triggered ${intentName}`; + const speakOutput = SpeakIntentReflectorHandlerP1 + `${intentName}` + SpeakIntentReflectorHandlerP2; return handlerInput.responseBuilder .speak(speakOutput) @@ -2767,7 +2786,7 @@ const ErrorHandler = { return true; }, handle(handlerInput, error) { - const speakOutput = 'Sorry, I had trouble doing what you asked. Please try again.'; + const speakOutput = SpeakErrorHandler; console.log(`~~~~ Error handled: ${JSON.stringify(error)}`); return handlerInput.responseBuilder @@ -2857,4 +2876,4 @@ exports.handler = Alexa.SkillBuilders.custom() }) ) */ - .lambda(); \ No newline at end of file + .lambda(); diff --git a/lambda/locale/de-DE.js b/lambda/locale/de-DE.js new file mode 100644 index 0000000..903939e --- /dev/null +++ b/lambda/locale/de-DE.js @@ -0,0 +1,19 @@ +// locale/de-de.js +module.exports = { + SpeakWelcome: 'Wilkommen bei Audiobookshelf.', + SpeakPlaying: 'Spiele', + SpeakBy: 'von', + SpeakTryAgain: 'Versuch es nochmal.', + SpeakPlaybackBookHandlerNoBookTitle: 'Das habe ich nicht verstanden. Versuch es mit "Ließ mir Titel von Autor vor".', + SpeakPlaybackBookHandlerNoBookFoundP1: 'Kein Buch mit dem Titel', + SpeakPlaybackBookHandlerNoBookFoundP2: 'gefunden. Versuch es nochmal.', + SpeakPlayBookIntentHandlerNoBookTitle: 'Das habe ich nicht verstanden. Versuch es mit "Ließ mir Titel von Autor vor".', + SpeakPlayBookIntentHandlerNoBookFound: "Kein entsprechendes Buch gefunden:", + SpeakUnsupportedAudioIntentHandler: 'Entschuldige, das kann ich noch nicht.', + SpeakHelpIntentHandler: 'Du kannst sagen "Ließ mir etwas vor" - wie kann ich helfen?', + SpeakCancelAndStopIntentHandlerGoodbye: 'Bis zum nächsten mal!', + SpeakFallbackIntentHandler: 'Entschuldige, das weiß ich nicht. Versuch es nochmal.', + SpeakIntentReflectorHandlerP1: 'Du hast gerade', + SpeakIntentReflectorHandlerP2: 'ausgelöst', + SpeakErrorHandler: 'Entschuldige, ich hatte ein Problem dabei. Versuch es nochmal.' +}; diff --git a/lambda/locale/en-US.js b/lambda/locale/en-US.js new file mode 100644 index 0000000..98adf07 --- /dev/null +++ b/lambda/locale/en-US.js @@ -0,0 +1,19 @@ +// locale/en-US.js +module.exports = { + SpeakWelcome: 'Welcome to Audiobookshelf, you can say "play audiobook" to start listening.', + SpeakPlaying: 'Playing', + SpeakBy: 'by', + SpeakTryAgain: 'Please try again.', + SpeakPlaybackBookHandlerNoBookTitle: 'I did not understand the request. For example, try saying "Play audiobook title by author".', + SpeakPlaybackBookHandlerNoBookFoundP1: '"No book of title', + SpeakPlaybackBookHandlerNoBookFoundP2: 'found. Please try again.', + SpeakPlayBookIntentHandlerNoBookTitle: 'I did not understand the request. For example, try saying "Play audiobook title by author".', + SpeakPlayBookIntentHandlerNoBookFound: "Could not find a playable book matching:", + SpeakUnsupportedAudioIntentHandler: 'Sorry, I can\'t support that yet.', + SpeakHelpIntentHandler: 'You can say "play audio" to start playing your book! How can I help?', + SpeakCancelAndStopIntentHandlerGoodbye: 'Goodbye!', + SpeakFallbackIntentHandler: 'Sorry, I don\'t know about that. Please try again.', + SpeakIntentReflectorHandlerP1: 'You just triggered', + SpeakIntentReflectorHandlerP2: '', + SpeakErrorHandler: 'Sorry, I had trouble doing what you asked. Please try again.' +}; diff --git a/skill-package/interactionModels/custom/de-DE.json b/skill-package/interactionModels/custom/de-DE.json new file mode 100644 index 0000000..6d8f710 --- /dev/null +++ b/skill-package/interactionModels/custom/de-DE.json @@ -0,0 +1,253 @@ +{ + "interactionModel": { + "dialog": { + "intents": [ + { + "slots": [ + { + "name": "time", + "elicitationRequired": true, + "type": "AMAZON.DURATION", + "confirmationRequired": false, + "prompts": { + "elicitation": "Elicit.Slot.454140924019.1491668277531" + } + } + ], + "name": "GoBackXTimeIntent", + "confirmationRequired": false, + "prompts": {} + }, + { + "slots": [ + { + "name": "time", + "elicitationRequired": true, + "type": "AMAZON.DURATION", + "confirmationRequired": false, + "prompts": { + "elicitation": "Elicit.Slot.1425758106737.86337652976" + } + } + ], + "name": "GoForwardXTimeIntent", + "confirmationRequired": false, + "prompts": {} + }, + { + "slots": [ + { + "name": "title", + "elicitationRequired": false, + "type": "AMAZON.Book", + "confirmationRequired": false, + "prompts": {} + }, + { + "name": "author", + "elicitationRequired": false, + "type": "AMAZON.Author", + "confirmationRequired": false, + "prompts": {} + } + ], + "name": "PlayBookIntent", + "confirmationRequired": false, + "prompts": {} + }, + { + "slots": [ + { + "name": "chapterNumber", + "elicitationRequired": true, + "type": "AMAZON.NUMBER", + "confirmationRequired": false, + "prompts": { + "elicitation": "Elicit.Slot.464967230894.1040650485539" + } + } + ], + "name": "GoToChapterX", + "confirmationRequired": false, + "prompts": {} + } + ], + "delegationStrategy": "ALWAYS" + }, + "languageModel": { + "intents": [ + { + "name": "AMAZON.CancelIntent", + "samples": [ + "Verlassen", + "Abbrechen" + ] + }, + { + "name": "AMAZON.HelpIntent", + "samples": [] + }, + { + "name": "AMAZON.StopIntent", + "samples": [] + }, + { + "slots": [], + "name": "HelloWorldIntent", + "samples": [ + "Hallo", + "Test" + ] + }, + { + "name": "AMAZON.NavigateHomeIntent", + "samples": [] + }, + { + "name": "AMAZON.FallbackIntent", + "samples": [] + }, + { + "slots": [], + "name": "PlayLastIntent", + "samples": [ + "Spiel das letzte Buch", + "weiter", + "Ließ mir das letzte Buch vor", + "Ließ mir was vor" + ] + }, + { + "name": "AMAZON.PauseIntent", + "samples": [] + }, + { + "name": "AMAZON.ResumeIntent", + "samples": [] + }, + { + "name": "AMAZON.PreviousIntent", + "samples": [] + }, + { + "slots": [ + { + "name": "time", + "type": "AMAZON.DURATION", + "samples": [ + "Spring {time} zurück", + "Spule {time} zurück", + "Gehe {time} zurück", + "{time} zurückspulen", + "{time} zurückgehen", + "{time} zurückspringen" + ] + } + ], + "name": "GoBackXTimeIntent", + "samples": [ + "Spring {time} zurück", + "Spule {time} zurück", + "Gehe {time} zurück", + "{time} zurückspulen", + "{time} zurückgehen", + "{time} zurückspringen" + ] + }, + { + "slots": [ + { + "name": "time", + "type": "AMAZON.DURATION", + "samples": [ + "Springe {time} vor" + ] + } + ], + "name": "GoForwardXTimeIntent", + "samples": [ + "Springe {time} vor", + "Überspring {time}", + "Spul {time} vor", + "Spring {time} nach vorne", + "Gehe {time} vor", + "{time} vorwärts", + "{time} überspringen", + "{time} vorspulen" + ] + }, + { + "slots": [ + { + "name": "title", + "type": "AMAZON.Book" + }, + { + "name": "author", + "type": "AMAZON.Author" + } + ], + "name": "PlayBookIntent", + "samples": [ + "gib {title} von {author} wieder", + "ließ mir {title} vor", + "ließ mir {title} von {author} vor", + "{title}", + "{title} von {author}", + "{author} {title}" + ] + }, + { + "slots": [ + { + "name": "chapterNumber", + "type": "AMAZON.NUMBER" + } + ], + "name": "GoToChapterX", + "samples": [ + "Gehe zu Kapitel {chapterNumber}", + "spring zu Kapitel {chapterNumber}", + "Spule zu Kapitel {chapterNumber} zurück", + "Spule zu Kapitel {chapterNumber} vor" + ] + }, + { + "name": "AMAZON.NextIntent", + "samples": [] + } + ], + "types": [], + "invocationName": "privates vorlesen" + }, + "prompts": [ + { + "variations": [ + { + "type": "PlainText", + "value": "Du kannst zum beispiel sagen gehe 15 sekunden zurück." + } + ], + "id": "Elicit.Slot.454140924019.1491668277531" + }, + { + "variations": [ + { + "type": "PlainText", + "value": "Du kannst zum beispiel sagen gehe 15 sekunden vor." + } + ], + "id": "Elicit.Slot.1425758106737.86337652976" + }, + { + "variations": [ + { + "type": "PlainText", + "value": "Du kannst zum beispiel sagen gehe zu kapitel fünf." + } + ], + "id": "Elicit.Slot.464967230894.1040650485539" + } + ] + } +} diff --git a/skill-package/skill.json b/skill-package/skill.json index 4d02a03..e7a9010 100644 --- a/skill-package/skill.json +++ b/skill-package/skill.json @@ -46,6 +46,18 @@ "keywords": [], "name": "Audiobookshelf", "summary": "Play audiobooks from self-hosted Audiobookshelf server" + }, + "de-DE": { + "description": "Angepasster Skill zur wiedergabe von Dateien aus einer öffentlichen, privaten Audiobookshelf-Instanz", + "examplePhrases": [ + "Weiter", + "Ließ mir etwas vor", + "Ließ mir Der Herr der Ringe von J.R.R. Tolkien vor", + "Spule 30 Sekunden vor" + ], + "keywords": [], + "name": "Audiobookshelf", + "summary": "Gib Hörbücher vom selbst gehosteten Audiobookshelf-Server wieder." } }, "testingInstructions": "Sample Testing Instructions."