-
-
Notifications
You must be signed in to change notification settings - Fork 198
Add niconico video Provider #2339
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Changes from 138 commits
80e31d0
973b8a4
dbb9876
3b29d27
64b0cec
373c82d
a0ade86
c04f7af
e8f8378
777455e
f7481d6
bf02f3e
9198bca
e2cdbe8
8e6f1c5
349b6de
2c25241
e9704e8
d7c6351
e6e8d90
57a7dd0
98ea0e7
2e6ea0a
eb3e41c
7003f31
44f711d
a611d6f
ed4e44c
a6090d5
7cfd0e3
f6138bb
bb1a7b4
c930829
9d52d6b
a421ada
defd6d1
d93036e
a31c470
4ae716d
826f108
d0009f8
c236877
3bfd635
5cbbb66
d308a4f
8ea40bd
651a9a3
b0d8665
720f084
bb1167a
065caed
92a1c3b
4353731
9de5049
b3cd52e
1b57812
935b16b
532755c
f8efb46
b14196f
0f37f2f
ac6368a
983ead0
ed5061a
79c816a
8edcfba
c2591d4
a0895d3
14e7b23
1a44a5c
c0be02f
99ad818
6ef632a
33f69e7
3cd3bdc
cda3914
b470355
77ce0bf
074f48a
fb90852
cad2f47
a0351dd
4a76366
a49705a
7e76be3
9213ccb
b18bc50
c8d0811
a3299b3
b439ded
ee175b4
5f1d3e5
c7ac91d
1dab66a
f43fb97
29a498e
e638054
f047372
bce3c23
906fb0d
88f258b
9f1ff99
8e9805b
33d8b83
9948c62
d9389bd
aa1acb0
e508065
cda9a25
380d9af
3d7fea1
6e6ff76
85fe0eb
ed143c9
d8bcf82
4ca29bd
c23c4aa
2da8daf
3082cc6
53d2e3c
1275d6e
710f363
5f0554c
3bb7856
eac2f4e
b1a4be4
ccb096c
2589f16
b205462
9ebd0f2
a33ffe0
36e30cc
1785eee
e76c0e6
23b3bd4
fa9b301
5804fb5
e7a93ca
e9da790
a3d8bc7
09e8bc9
a52d7e3
ee05c48
da5f9c6
232f76f
17363eb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,119 @@ | ||
| name: nicovideo Provider Fixtures Update and Test | ||
| description: "To use it, you need to set the GitHub secret variable NICONICO_SESSION to the test account session." | ||
|
|
||
| on: | ||
| # Daily execution | ||
| # Requires GitHub repository variable ENABLE_NICOVIDEO_SCHEDULABLE_FIXTURE_UPDATES to enable scheduled fixture updates | ||
| schedule: | ||
| - cron: '00 0 * * *' | ||
|
|
||
| # Manual execution | ||
| workflow_dispatch: | ||
| inputs: | ||
| provider: | ||
| description: 'Provider to update fixtures for' | ||
| required: true | ||
| default: 'nicovideo' | ||
| type: choice | ||
| options: | ||
| - nicovideo | ||
|
|
||
| run_tests: | ||
| description: 'Run tests after updating fixtures' | ||
| required: true | ||
| default: true | ||
| type: boolean | ||
|
|
||
| env: | ||
| PYTHON_VERSION: "3.12" | ||
|
|
||
| jobs: | ||
| setup-and-update-fixtures: | ||
| name: Update NicoVideo Fixtures and Run Tests | ||
| runs-on: ubuntu-latest | ||
| if: (vars.ENABLE_NICOVIDEO_SCHEDULABLE_FIXTURE_UPDATES == 'true' && github.event_name == 'schedule') || (github.event.inputs.provider == 'nicovideo' || github.event_name == 'push') | ||
|
|
||
| steps: | ||
| - name: Checkout repository | ||
| uses: actions/checkout@v4 | ||
|
|
||
| - name: Set up Python ${{ env.PYTHON_VERSION }} | ||
| uses: actions/[email protected] | ||
| with: | ||
| python-version: ${{ env.PYTHON_VERSION }} | ||
|
|
||
| - name: Cache Python dependencies | ||
| uses: actions/cache@v4 | ||
| with: | ||
| path: | | ||
| ~/.cache/pip | ||
| key: ${{ runner.os }}-python-${{ env.PYTHON_VERSION }}-${{ hashFiles('**/pyproject.toml', '**/requirements*.txt') }} | ||
| restore-keys: | | ||
| ${{ runner.os }}-python-${{ env.PYTHON_VERSION }}- | ||
| ${{ runner.os }}-python- | ||
|
|
||
| - name: Install system dependencies | ||
| run: | | ||
| sudo apt-get update | ||
| sudo apt-get install -y ffmpeg | ||
|
|
||
| - name: Install Python dependencies | ||
| run: | | ||
| python -m pip install --upgrade pip build setuptools | ||
| pip install .[server] .[test] -r requirements_all.txt | ||
|
|
||
| - name: Verify environment setup | ||
| run: | | ||
| python --version | ||
| pip --version | ||
| echo "Environment setup completed successfully" | ||
|
|
||
| - name: Update NicoVideo fixtures | ||
| env: | ||
| NICONICO_SESSION: ${{ secrets.NICONICO_SESSION }} | ||
| run: | | ||
| cd tests/providers/nicovideo/fixtures/scripts | ||
| set -o pipefail | ||
| python main.py 2>&1 | tee fixture-update.log | ||
|
|
||
| - name: Check for fixture changes | ||
| id: check_changes | ||
| run: | | ||
| if git diff --quiet; then | ||
| echo "changes=false" >> $GITHUB_OUTPUT | ||
| echo "No fixture changes detected" | ||
| else | ||
| echo "changes=true" >> $GITHUB_OUTPUT | ||
| echo "Fixture changes detected" | ||
| git diff --name-only | ||
| fi | ||
|
|
||
| - name: Run NicoVideo Tests | ||
| if: steps.check_changes.outputs.changes == 'true' && (github.event.inputs.run_tests == 'true' || github.event_name == 'schedule' || github.event_name == 'push') | ||
| run: | | ||
| pytest --durations 10 \ | ||
| --cov-report term-missing \ | ||
| --cov=music_assistant.providers.nicovideo \ | ||
| --cov-report=xml \ | ||
| tests/providers/nicovideo/ \ | ||
| -v \ | ||
| --tb=short | ||
|
|
||
| - name: Upload fixture artifacts | ||
| if: failure() || steps.check_changes.outputs.changes == 'true' | ||
| uses: actions/upload-artifact@v4 | ||
| with: | ||
| name: updated-nicovideo-fixtures | ||
| path: tests/providers/nicovideo/fixtures/scripts/fixture-update.log | ||
| retention-days: 30 | ||
|
|
||
| - name: Upload test results | ||
| if: failure() | ||
| uses: actions/upload-artifact@v4 | ||
| with: | ||
| name: test-results | ||
| path: pytest-results.xml | ||
| retention-days: 30 | ||
|
|
||
| outputs: | ||
| fixtures-updated: ${{ steps.check_changes.outputs.changes }} |
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. please limit your PR to you provider only. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Removed the VS Code launch configuration changes. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| """nicovideo support for Music Assistant.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from typing import TYPE_CHECKING | ||
|
|
||
| from music_assistant_models.enums import ProviderFeature | ||
|
|
||
| from music_assistant.mass import MusicAssistant | ||
| from music_assistant.models import ProviderInstanceType | ||
| from music_assistant.providers.nicovideo.config import get_config_entries_impl | ||
| from music_assistant.providers.nicovideo.provider import NicovideoMusicProvider | ||
|
|
||
| if TYPE_CHECKING: | ||
| from music_assistant_models.config_entries import ( | ||
| ConfigEntry, | ||
| ConfigValueType, | ||
| ProviderConfig, | ||
| ) | ||
| from music_assistant_models.provider import ProviderManifest | ||
|
|
||
| # Supported features collected from all mixins | ||
| SUPPORTED_FEATURES = { | ||
| # Track mixin | ||
| ProviderFeature.LIBRARY_TRACKS, | ||
| ProviderFeature.LIBRARY_TRACKS_EDIT, | ||
| # Artist mixin | ||
| ProviderFeature.ARTIST_TOPTRACKS, | ||
| ProviderFeature.ARTIST_ALBUMS, | ||
| ProviderFeature.LIBRARY_ARTISTS, | ||
| ProviderFeature.LIBRARY_ARTISTS_EDIT, | ||
| # Album mixin | ||
| ProviderFeature.LIBRARY_ALBUMS, | ||
| # Playlist mixin | ||
| ProviderFeature.LIBRARY_PLAYLISTS, | ||
| ProviderFeature.PLAYLIST_TRACKS_EDIT, | ||
| ProviderFeature.PLAYLIST_CREATE, | ||
| # Explorer mixin | ||
| ProviderFeature.SEARCH, | ||
| ProviderFeature.RECOMMENDATIONS, | ||
| ProviderFeature.SIMILAR_TRACKS, | ||
| } | ||
|
|
||
|
|
||
| async def setup( | ||
| mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig | ||
| ) -> ProviderInstanceType: | ||
| """Initialize provider(instance) with given configuration.""" | ||
| return NicovideoMusicProvider(mass, manifest, config, SUPPORTED_FEATURES) | ||
|
|
||
|
|
||
| async def get_config_entries( | ||
| mass: MusicAssistant, # noqa: ARG001 | ||
| instance_id: str | None = None, # noqa: ARG001 | ||
| action: str | None = None, # noqa: ARG001 | ||
| values: dict[str, ConfigValueType] | None = None, # noqa: ARG001 | ||
| ) -> tuple[ConfigEntry, ...]: | ||
| """Return Config entries to setup this provider.""" | ||
| return await get_config_entries_impl() |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| """Nicovideo provider configuration system.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from typing import TYPE_CHECKING | ||
|
|
||
| from .categories import AuthConfigCategory | ||
| from .factory import get_config_entries_impl | ||
|
|
||
| if TYPE_CHECKING: | ||
| from music_assistant.models.provider import Provider | ||
|
|
||
|
|
||
| class NicovideoConfig: | ||
| """Configuration system for Nicovideo provider.""" | ||
|
|
||
| def __init__(self, provider: Provider) -> None: | ||
| """Initialize with all category instances.""" | ||
| self.auth = AuthConfigCategory(provider) | ||
|
|
||
|
|
||
| __all__ = [ | ||
| "NicovideoConfig", | ||
| "get_config_entries_impl", | ||
| ] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| """Configuration categories for Nicovideo provider.""" | ||
|
|
||
| from .auth import AuthConfigCategory | ||
| from .base import ConfigCategoryBase | ||
|
|
||
| __all__ = [ | ||
| "AuthConfigCategory", | ||
| "ConfigCategoryBase", | ||
| ] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| """Authentication configuration category for Nicovideo provider.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from music_assistant.providers.nicovideo.config.categories.base import ConfigCategoryBase | ||
| from music_assistant.providers.nicovideo.config.factory import ConfigFactory | ||
|
|
||
|
|
||
| class AuthConfigCategory(ConfigCategoryBase): | ||
| """Authentication settings category.""" | ||
|
|
||
| _auth = ConfigFactory("Authentication") | ||
|
|
||
| mail = _auth.str_config( | ||
| key="mail", | ||
| label="Email", | ||
| default=None, | ||
| description="Your NicoNico account email address.", | ||
| ) | ||
|
|
||
| password = _auth.secure_str_or_none_config( | ||
| key="password", | ||
| label="Password", | ||
| description="Your NicoNico account password.", | ||
| ) | ||
|
|
||
|
Comment on lines
+9
to
+26
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So, are your config entries really that large complex that you need this multi layered pattern of providing the config entries ? Can you give some insights about the number of config entries ? I would always start with a small/simple setup/PR and then extend later when/if needed. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Right. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've simplified the configuration as requested. The Content and Recommendations categories have been removed, leaving only the essential Authentication category (email/password). Changes made in commits 85fe0eb and ed143c9:
The complex configuration has been preserved in a separate branch ( This makes the provider much simpler to understand and maintain for initial users. |
||
| mfa = _auth.str_config( | ||
| key="mfa", | ||
| label="MFA Code (One-Time Password)", | ||
| default=None, | ||
| description="Enter the 6-digit confirmation code from your 2-step verification app.", | ||
| ) | ||
|
|
||
| user_session = _auth.secure_str_or_none_config( | ||
| key="user_session", | ||
| label="User Session ( 'user_session' in Cookie)", | ||
| description=( | ||
| "Enter the user_session cookie value.\n" | ||
| "If invalid, it will be automatically set from your email and password." | ||
| ), | ||
| ) | ||
|
|
||
| def save_user_session(self, value: str) -> None: | ||
| """Save user session to config.""" | ||
| self.writer.set_raw_provider_config_value( | ||
| self.provider.instance_id, | ||
| "user_session", | ||
| value, | ||
| True, | ||
| ) | ||
|
|
||
| def clear_mfa_code(self) -> None: | ||
| """Clear MFA code after successful use (one-time password should not be reused).""" | ||
| self.writer.set_raw_provider_config_value( | ||
| self.provider.instance_id, | ||
| "mfa", | ||
| None, | ||
| True, | ||
| ) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| """Base class for configuration categories.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from typing import TYPE_CHECKING, override | ||
|
|
||
| from music_assistant.controllers.config import ConfigController | ||
| from music_assistant.providers.nicovideo.config.descriptor import ConfigReader | ||
|
|
||
| if TYPE_CHECKING: | ||
| from music_assistant_models.config_entries import ConfigValueType, ProviderConfig | ||
|
|
||
| from music_assistant.models.provider import Provider | ||
|
|
||
|
|
||
| class ConfigCategoryBase(ConfigReader): | ||
| """Base class for config categories.""" | ||
|
|
||
| def __init__(self, provider: Provider) -> None: | ||
| """Initialize category with provider instance.""" | ||
| self.provider = provider | ||
|
|
||
| @property | ||
| def reader(self) -> ProviderConfig: | ||
| """Get the config reader interface.""" | ||
| return self.provider.config | ||
|
|
||
| @property | ||
| def writer(self) -> ConfigController: | ||
| """Get the config writer interface.""" | ||
| return self.provider.mass.config | ||
|
|
||
| @override | ||
| def get_value(self, key: str) -> ConfigValueType: | ||
| """Get config value from provider.""" | ||
| return self.reader.get_value(key) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| """Configuration descriptor implementation for Nicovideo provider.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from collections.abc import Callable | ||
| from typing import TYPE_CHECKING, Protocol | ||
|
|
||
| if TYPE_CHECKING: | ||
| from music_assistant_models.config_entries import ConfigEntry, ConfigValueType | ||
|
|
||
|
|
||
| class ConfigReader(Protocol): | ||
| """Protocol for configuration readers.""" | ||
|
|
||
| def get_value(self, key: str) -> ConfigValueType: | ||
| """Retrieve a configuration value by key.""" | ||
| ... | ||
|
|
||
|
|
||
| class ConfigDescriptor[T]: | ||
| """Typed config descriptor with embedded ConfigEntry.""" | ||
|
|
||
| def __init__( | ||
| self, | ||
| cast: Callable[[ConfigValueType], T], | ||
| config_entry: ConfigEntry, | ||
| ) -> None: | ||
| """Initialize descriptor. | ||
|
|
||
| Args: | ||
| cast: Transformation/validation applied to raw value. | ||
| config_entry: ConfigEntry definition for this option. | ||
| """ | ||
| self.cast = cast | ||
| self.config_entry = config_entry | ||
|
|
||
| @property | ||
| def key(self) -> str: | ||
| """Get the config key from the embedded ConfigEntry.""" | ||
| return self.config_entry.key | ||
|
|
||
| def __get__(self, instance: ConfigReader, owner: type) -> T: | ||
| """Descriptor access.""" | ||
| raw = instance.get_value(self.key) | ||
| return self.cast(raw) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
please remove this from the PR, if you need to do specific CI tasks and testing of your code for translating api to python models, that needs to be in a dedicated repository for your client library.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the feedback. Removed the CI workflow and all fixture generation infrastructure from this PR.
Only test execution code and static fixtures remain.