infinityofspace / jellyfin_alexa_skill

Selfhosted Alexa media player skill for Jellyfin
GNU General Public License v3.0
71 stars 11 forks source link

have to add about Windows base running. #122

Open po2son opened 1 month ago

po2son commented 1 month ago

Please describe which problem should be solved

Error msg like this coz window not include 'fcntl' [comment]:

    jellyfin_alexa_skill --config /path/to/skill.conf --data /path/to/skill/data/
    Traceback (most recent call last):
      File "<frozen runpy>", line 198, in _run_module_as_main
      File "<frozen runpy>", line 88, in _run_code
      File "C:\Python312\Scripts\jellyfin_alexa_skill.exe\__main__.py", line 4, in <module>
      File "C:\Python312\Lib\site-packages\jellyfin_alexa_skill\main.py", line 21, in <module>
        from gunicorn.app.base import BaseApplication
      File "C:\Python312\Lib\site-packages\gunicorn\app\base.py", line 11, in <module>
        from gunicorn import util
      File "C:\Python312\Lib\site-packages\gunicorn\util.py", line 8, in <module>
        import fcntl
    ModuleNotFoundError: No module named 'fcntl'

Optional: Describe your idea for a solution

So have to change to 'waitress' [comment]:

jellyfin-alexa-skill\main.py have to change like this.

import argparse
import binascii
import logging
import os
import textwrap
import uuid
from configparser import ConfigParser
from copy import deepcopy
from pathlib import Path
from typing import Union, Tuple

import ask_sdk_model_runtime
from ask_smapi_model.services.skill_management import SkillManagementServiceClient
from ask_smapi_model.v1.skill.account_linking import AccountLinkingRequest, AccountLinkingRequestPayload, AccountLinkingType
from ask_smapi_model.v1.skill.manifest import SSLCertificateType, SkillManifestEndpoint, SkillManifestEnvelope
from ask_smapi_sdk import StandardSmapiClientBuilder
from flask import Flask
from flask_ask_sdk.skill_adapter import SkillAdapter
from flask_wtf import CSRFProtect
# from gunicorn.app.base import BaseApplication
from pyngrok import conf, ngrok

from jellyfin_alexa_skill import __version__
from jellyfin_alexa_skill.alexa.handler import get_skill_builder
from jellyfin_alexa_skill.alexa.setup.interaction.model import INTERACTION_MODELS
from jellyfin_alexa_skill.alexa.setup.manifest.manifest import get_skill_version, SKILL_MANIFEST
from jellyfin_alexa_skill.alexa.web.skill import get_skill_blueprint
from jellyfin_alexa_skill.config import get_config, DEFAULT_ALEXA_SKILL_CONFIG_PATH, DEFAULT_ALEXA_SKILL_DATA_PATH, APP_NAME, write_config
from jellyfin_alexa_skill.database.db import connect_db
from jellyfin_alexa_skill.jellyfin.api.client import JellyfinClient
from jellyfin_alexa_skill.jellyfin.web.login import get_jellyfin_login_blueprint

from waitress import serve

logging.basicConfig(format="%(levelname)s:%(message)s", level=logging.INFO)

def main():
    parser = argparse.ArgumentParser(
        description="Jellyfin Alexa Skill",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog=textwrap.dedent('''\
            Example:
                jellyfin_alexa_skill --config /path/to/skill.conf --data /path/to/skill/data/
            '''))
    parser.add_argument('--config', type=str, default=DEFAULT_ALEXA_SKILL_CONFIG_PATH,
                        help='path to the config file')
    parser.add_argument('--data', type=str, default=DEFAULT_ALEXA_SKILL_DATA_PATH,
                        help='path to the data directory')
    parser.add_argument('--setup', action='store_true',
                        help='run skill setup')
    parser.add_argument('--ngrok', action='store_true',
                        help='start ngrok tunnel for development')
    parser.add_argument('--port', type=int, default=None,
                        help='port to run the web application')
    args = parser.parse_args()

    config_path = Path(args.config)
    data_path = Path(args.data)

    config = get_config(config_path)
    csrf = CSRFProtect()

    app = Flask(APP_NAME)
    app.config['SECRET_KEY'] = binascii.hexlify(os.urandom(24))
    csrf.init_app(app)

    jellyfin_endpoint = config["general"]["jellyfin_endpoint"]
    host = config["general"].get("host", "127.0.0.1")

    if args.ngrok:
        conf.get_default().region = config["ngrok"].get("region", "us")
        public_url = ngrok.connect(args.port if args.port else config["general"]["web_app_port"])
        logging.info(" * ngrok tunnel \"{}\" -> \"http://127.0.0.1:{}/\"".format(public_url, args.port))

    if args.setup:
        setup_skill(config, config_path, data_path, app, csrf, jellyfin_endpoint, host)
    else:
        data_path.mkdir(parents=True, exist_ok=True)
        connect_db(data_path / "data.sqlite")
        jellyfin_client = JellyfinClient(server_endpoint=jellyfin_endpoint, client_name=APP_NAME)
        skill_adapter = SkillAdapter(skill=get_skill_builder(jellyfin_client).create(), skill_id=config["general"]["skill_id"], app=app)
        skill_blueprint = get_skill_blueprint(skill_adapter)
        csrf.exempt(skill_blueprint)
        app.register_blueprint(skill_blueprint)
        login_blueprint = get_jellyfin_login_blueprint(jellyfin_endpoint, config["smapi"]["client_id"])
        app.register_blueprint(login_blueprint)

        port = args.port if args.port else config["general"]["web_app_port"]
        serve(app, host=host, port=port)

def setup_skill(config, config_path, data_path, app, csrf, jellyfin_endpoint, host):
    smapi_client = get_smapi_client(config["smapi"]["client_id"], config["smapi"]["client_secret"])
    stage = config["general"].get("stage", "development")
    skill_id = config["general"]["skill_id"]

    if skill_id is None:
        skill_id = create_skill(smapi_client, config["smapi"]["vendor_id"], stage)
        config["general"]["skill_id"] = skill_id
        write_config(config, config_path)
        logging.info(f"Created new skill with id: {skill_id}")

    skill_endpoint = SkillManifestEndpoint(
        uri=f"{host}/api/alexa",
        ssl_certificate_type=SSLCertificateType.SELF_SIGNED
    )

    manifest = deepcopy(SKILL_MANIFEST)
    manifest["apis"]["custom"]["endpoint"] = skill_endpoint

    account_linking_request = get_account_linking_request(config, host)
    smapi_client.call_create_account_linking(skill_id, account_linking_request, stage)
    smapi_client.call_update_skill_manifest(skill_id, SkillManifestEnvelope(manifest), stage)
    logging.info(f"Updated skill manifest for skill id: {skill_id}")

    logging.info(" * Skill setup completed")

    data_path.mkdir(parents=True, exist_ok=True)
    connect_db(data_path / "data.sqlite")

    jellyfin_client = JellyfinClient(server_endpoint=jellyfin_endpoint, client_name=APP_NAME)
    skill_adapter = SkillAdapter(skill=get_skill_builder(jellyfin_client).create(), skill_id=skill_id, app=app)
    skill_blueprint = get_skill_blueprint(skill_adapter)
    csrf.exempt(skill_blueprint)
    app.register_blueprint(skill_blueprint)

    login_blueprint = get_jellyfin_login_blueprint(jellyfin_endpoint, config["smapi"]["client_id"])
    app.register_blueprint(login_blueprint)

    port = config["general"]["web_app_port"]
    serve(app, host=host, port=port)

def get_smapi_client(client_id: str, client_secret: str) -> SkillManagementServiceClient:
    api_endpoint = os.getenv("ASK_SMAPI_SERVER", "https://api.amazonalexa.com")
    return StandardSmapiClientBuilder().with_client_id(client_id).with_client_secret(client_secret).with_refresh_token(uuid.uuid4()).with_api_endpoint(api_endpoint).build()

def create_skill(smapi_client: SkillManagementServiceClient, vendor_id: str, stage: str) -> str:
    manifest = deepcopy(SKILL_MANIFEST)
    response = smapi_client.call_create_skill_v1(SkillManifestEnvelope(manifest), vendor_id, stage)
    return response["skillId"]

def get_account_linking_request(config: ConfigParser, host: str) -> AccountLinkingRequest:
    return AccountLinkingRequest(
        account_linking_request_payload=AccountLinkingRequestPayload(
            access_token_uri=config["smapi"]["access_token_uri"],
            client_id=config["smapi"]["client_id"],
            scopes=config["smapi"]["scopes"],
            client_secret=config["smapi"]["client_secret"],
            authorization_uri=config["smapi"]["authorization_uri"],
            domains=config["smapi"]["domains"],
            redirect_urls=[f"{host}/authresponse"],
            access_token_scheme="HTTP_BASIC",
            default_access_token_expiration_seconds=config["smapi"].getint("default_access_token_expiration_seconds", 3600),
            grant_type=AccountLinkingType.AUTH_CODE,
        )
    )

if __name__ == "__main__":
    main()

and run

    python ./jellyfin_alexa_skill/main.py --config D:/work/jellyfin_alexa_skill/skill.conf --data D:/work/jellyfin_alexa_skill/data/

I solved like this.