mkv-episode-matcher 0.9.4__tar.gz → 0.9.6__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of mkv-episode-matcher might be problematic. Click here for more details.
- mkv_episode_matcher-0.9.6/.claude/settings.local.json +20 -0
- {mkv_episode_matcher-0.9.4 → mkv_episode_matcher-0.9.6}/.coverage +0 -0
- mkv_episode_matcher-0.9.6/.github/workflows/claude-code-review.yml +78 -0
- mkv_episode_matcher-0.9.6/.github/workflows/claude.yml +64 -0
- {mkv_episode_matcher-0.9.4 → mkv_episode_matcher-0.9.6}/PKG-INFO +1 -1
- {mkv_episode_matcher-0.9.4 → mkv_episode_matcher-0.9.6}/mkv_episode_matcher/config.py +4 -4
- {mkv_episode_matcher-0.9.4 → mkv_episode_matcher-0.9.6}/mkv_episode_matcher/episode_identification.py +5 -3
- {mkv_episode_matcher-0.9.4 → mkv_episode_matcher-0.9.6}/mkv_episode_matcher.egg-info/PKG-INFO +1 -1
- {mkv_episode_matcher-0.9.4 → mkv_episode_matcher-0.9.6}/mkv_episode_matcher.egg-info/SOURCES.txt +5 -0
- {mkv_episode_matcher-0.9.4 → mkv_episode_matcher-0.9.6}/setup.cfg +1 -1
- mkv_episode_matcher-0.9.6/tests/test_config_special_characters.py +174 -0
- mkv_episode_matcher-0.9.6/tests/test_episode_identification.py +286 -0
- {mkv_episode_matcher-0.9.4 → mkv_episode_matcher-0.9.6}/.gitattributes +0 -0
- {mkv_episode_matcher-0.9.4 → mkv_episode_matcher-0.9.6}/.github/funding.yml +0 -0
- {mkv_episode_matcher-0.9.4 → mkv_episode_matcher-0.9.6}/.github/workflows/documentation.yml +0 -0
- {mkv_episode_matcher-0.9.4 → mkv_episode_matcher-0.9.6}/.github/workflows/python-publish.yml +0 -0
- {mkv_episode_matcher-0.9.4 → mkv_episode_matcher-0.9.6}/.github/workflows/tests.yml +0 -0
- {mkv_episode_matcher-0.9.4 → mkv_episode_matcher-0.9.6}/.gitignore +0 -0
- {mkv_episode_matcher-0.9.4 → mkv_episode_matcher-0.9.6}/.gitmodules +0 -0
- {mkv_episode_matcher-0.9.4 → mkv_episode_matcher-0.9.6}/.python-version +0 -0
- {mkv_episode_matcher-0.9.4 → mkv_episode_matcher-0.9.6}/.vscode/settings.json +0 -0
- {mkv_episode_matcher-0.9.4 → mkv_episode_matcher-0.9.6}/CHANGELOG.md +0 -0
- {mkv_episode_matcher-0.9.4 → mkv_episode_matcher-0.9.6}/LICENSE +0 -0
- {mkv_episode_matcher-0.9.4 → mkv_episode_matcher-0.9.6}/README.md +0 -0
- {mkv_episode_matcher-0.9.4 → mkv_episode_matcher-0.9.6}/docs/api/index.md +0 -0
- {mkv_episode_matcher-0.9.4 → mkv_episode_matcher-0.9.6}/docs/changelog.md +0 -0
- {mkv_episode_matcher-0.9.4 → mkv_episode_matcher-0.9.6}/docs/cli.md +0 -0
- {mkv_episode_matcher-0.9.4 → mkv_episode_matcher-0.9.6}/docs/configuration.md +0 -0
- {mkv_episode_matcher-0.9.4 → mkv_episode_matcher-0.9.6}/docs/installation.md +0 -0
- {mkv_episode_matcher-0.9.4 → mkv_episode_matcher-0.9.6}/docs/quickstart.md +0 -0
- {mkv_episode_matcher-0.9.4 → mkv_episode_matcher-0.9.6}/docs/tips.md +0 -0
- {mkv_episode_matcher-0.9.4 → mkv_episode_matcher-0.9.6}/mkdocs.yml +0 -0
- {mkv_episode_matcher-0.9.4 → mkv_episode_matcher-0.9.6}/mkv_episode_matcher/.gitattributes +0 -0
- {mkv_episode_matcher-0.9.4 → mkv_episode_matcher-0.9.6}/mkv_episode_matcher/__init__.py +0 -0
- {mkv_episode_matcher-0.9.4 → mkv_episode_matcher-0.9.6}/mkv_episode_matcher/__main__.py +0 -0
- {mkv_episode_matcher-0.9.4 → mkv_episode_matcher-0.9.6}/mkv_episode_matcher/episode_matcher.py +0 -0
- {mkv_episode_matcher-0.9.4 → mkv_episode_matcher-0.9.6}/mkv_episode_matcher/subtitle_utils.py +0 -0
- {mkv_episode_matcher-0.9.4 → mkv_episode_matcher-0.9.6}/mkv_episode_matcher/tmdb_client.py +0 -0
- {mkv_episode_matcher-0.9.4 → mkv_episode_matcher-0.9.6}/mkv_episode_matcher/utils.py +0 -0
- {mkv_episode_matcher-0.9.4 → mkv_episode_matcher-0.9.6}/mkv_episode_matcher.egg-info/dependency_links.txt +0 -0
- {mkv_episode_matcher-0.9.4 → mkv_episode_matcher-0.9.6}/mkv_episode_matcher.egg-info/entry_points.txt +0 -0
- {mkv_episode_matcher-0.9.4 → mkv_episode_matcher-0.9.6}/mkv_episode_matcher.egg-info/requires.txt +0 -0
- {mkv_episode_matcher-0.9.4 → mkv_episode_matcher-0.9.6}/mkv_episode_matcher.egg-info/top_level.txt +0 -0
- {mkv_episode_matcher-0.9.4 → mkv_episode_matcher-0.9.6}/pyproject.toml +0 -0
- {mkv_episode_matcher-0.9.4 → mkv_episode_matcher-0.9.6}/setup.py +0 -0
- {mkv_episode_matcher-0.9.4 → mkv_episode_matcher-0.9.6}/tests/__init__.py +0 -0
- {mkv_episode_matcher-0.9.4 → mkv_episode_matcher-0.9.6}/tests/test_main.py +0 -0
- {mkv_episode_matcher-0.9.4 → mkv_episode_matcher-0.9.6}/tests/test_path_handling.py +0 -0
- {mkv_episode_matcher-0.9.4 → mkv_episode_matcher-0.9.6}/tests/test_trailing_slash.py +0 -0
- {mkv_episode_matcher-0.9.4 → mkv_episode_matcher-0.9.6}/uv.lock +0 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"permissions": {
|
|
3
|
+
"allow": [
|
|
4
|
+
"Bash(gh issue view:*)",
|
|
5
|
+
"Bash(git checkout:*)",
|
|
6
|
+
"Bash(python -m pytest tests/ -v)",
|
|
7
|
+
"Bash(uv run pytest:*)",
|
|
8
|
+
"Bash(gh pr view:*)",
|
|
9
|
+
"Bash(gh pr checks:*)",
|
|
10
|
+
"WebFetch(domain:github.com)",
|
|
11
|
+
"Bash(gh run view:*)",
|
|
12
|
+
"Bash(uv run:*)",
|
|
13
|
+
"Bash(git add:*)",
|
|
14
|
+
"Bash(git commit:*)",
|
|
15
|
+
"Bash(git push:*)",
|
|
16
|
+
"Bash(rm:*)"
|
|
17
|
+
],
|
|
18
|
+
"deny": []
|
|
19
|
+
}
|
|
20
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
name: Claude Code Review
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
types: [opened, synchronize]
|
|
6
|
+
# Optional: Only run on specific file changes
|
|
7
|
+
# paths:
|
|
8
|
+
# - "src/**/*.ts"
|
|
9
|
+
# - "src/**/*.tsx"
|
|
10
|
+
# - "src/**/*.js"
|
|
11
|
+
# - "src/**/*.jsx"
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
claude-review:
|
|
15
|
+
# Optional: Filter by PR author
|
|
16
|
+
# if: |
|
|
17
|
+
# github.event.pull_request.user.login == 'external-contributor' ||
|
|
18
|
+
# github.event.pull_request.user.login == 'new-developer' ||
|
|
19
|
+
# github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR'
|
|
20
|
+
|
|
21
|
+
runs-on: ubuntu-latest
|
|
22
|
+
permissions:
|
|
23
|
+
contents: read
|
|
24
|
+
pull-requests: read
|
|
25
|
+
issues: read
|
|
26
|
+
id-token: write
|
|
27
|
+
|
|
28
|
+
steps:
|
|
29
|
+
- name: Checkout repository
|
|
30
|
+
uses: actions/checkout@v4
|
|
31
|
+
with:
|
|
32
|
+
fetch-depth: 1
|
|
33
|
+
|
|
34
|
+
- name: Run Claude Code Review
|
|
35
|
+
id: claude-review
|
|
36
|
+
uses: anthropics/claude-code-action@beta
|
|
37
|
+
with:
|
|
38
|
+
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
|
39
|
+
|
|
40
|
+
# Optional: Specify model (defaults to Claude Sonnet 4, uncomment for Claude Opus 4)
|
|
41
|
+
# model: "claude-opus-4-20250514"
|
|
42
|
+
|
|
43
|
+
# Direct prompt for automated review (no @claude mention needed)
|
|
44
|
+
direct_prompt: |
|
|
45
|
+
Please review this pull request and provide feedback on:
|
|
46
|
+
- Code quality and best practices
|
|
47
|
+
- Potential bugs or issues
|
|
48
|
+
- Performance considerations
|
|
49
|
+
- Security concerns
|
|
50
|
+
- Test coverage
|
|
51
|
+
|
|
52
|
+
Be constructive and helpful in your feedback.
|
|
53
|
+
|
|
54
|
+
# Optional: Use sticky comments to make Claude reuse the same comment on subsequent pushes to the same PR
|
|
55
|
+
# use_sticky_comment: true
|
|
56
|
+
|
|
57
|
+
# Optional: Customize review based on file types
|
|
58
|
+
# direct_prompt: |
|
|
59
|
+
# Review this PR focusing on:
|
|
60
|
+
# - For TypeScript files: Type safety and proper interface usage
|
|
61
|
+
# - For API endpoints: Security, input validation, and error handling
|
|
62
|
+
# - For React components: Performance, accessibility, and best practices
|
|
63
|
+
# - For tests: Coverage, edge cases, and test quality
|
|
64
|
+
|
|
65
|
+
# Optional: Different prompts for different authors
|
|
66
|
+
# direct_prompt: |
|
|
67
|
+
# ${{ github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR' &&
|
|
68
|
+
# 'Welcome! Please review this PR from a first-time contributor. Be encouraging and provide detailed explanations for any suggestions.' ||
|
|
69
|
+
# 'Please provide a thorough code review focusing on our coding standards and best practices.' }}
|
|
70
|
+
|
|
71
|
+
# Optional: Add specific tools for running tests or linting
|
|
72
|
+
# allowed_tools: "Bash(npm run test),Bash(npm run lint),Bash(npm run typecheck)"
|
|
73
|
+
|
|
74
|
+
# Optional: Skip review for certain conditions
|
|
75
|
+
# if: |
|
|
76
|
+
# !contains(github.event.pull_request.title, '[skip-review]') &&
|
|
77
|
+
# !contains(github.event.pull_request.title, '[WIP]')
|
|
78
|
+
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
name: Claude Code
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
issue_comment:
|
|
5
|
+
types: [created]
|
|
6
|
+
pull_request_review_comment:
|
|
7
|
+
types: [created]
|
|
8
|
+
issues:
|
|
9
|
+
types: [opened, assigned]
|
|
10
|
+
pull_request_review:
|
|
11
|
+
types: [submitted]
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
claude:
|
|
15
|
+
if: |
|
|
16
|
+
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
|
|
17
|
+
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
|
|
18
|
+
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
|
|
19
|
+
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
|
|
20
|
+
runs-on: ubuntu-latest
|
|
21
|
+
permissions:
|
|
22
|
+
contents: read
|
|
23
|
+
pull-requests: read
|
|
24
|
+
issues: read
|
|
25
|
+
id-token: write
|
|
26
|
+
actions: read # Required for Claude to read CI results on PRs
|
|
27
|
+
steps:
|
|
28
|
+
- name: Checkout repository
|
|
29
|
+
uses: actions/checkout@v4
|
|
30
|
+
with:
|
|
31
|
+
fetch-depth: 1
|
|
32
|
+
|
|
33
|
+
- name: Run Claude Code
|
|
34
|
+
id: claude
|
|
35
|
+
uses: anthropics/claude-code-action@beta
|
|
36
|
+
with:
|
|
37
|
+
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
|
38
|
+
|
|
39
|
+
# This is an optional setting that allows Claude to read CI results on PRs
|
|
40
|
+
additional_permissions: |
|
|
41
|
+
actions: read
|
|
42
|
+
|
|
43
|
+
# Optional: Specify model (defaults to Claude Sonnet 4, uncomment for Claude Opus 4)
|
|
44
|
+
# model: "claude-opus-4-20250514"
|
|
45
|
+
|
|
46
|
+
# Optional: Customize the trigger phrase (default: @claude)
|
|
47
|
+
# trigger_phrase: "/claude"
|
|
48
|
+
|
|
49
|
+
# Optional: Trigger when specific user is assigned to an issue
|
|
50
|
+
# assignee_trigger: "claude-bot"
|
|
51
|
+
|
|
52
|
+
# Optional: Allow Claude to run specific commands
|
|
53
|
+
# allowed_tools: "Bash(npm install),Bash(npm run build),Bash(npm run test:*),Bash(npm run lint:*)"
|
|
54
|
+
|
|
55
|
+
# Optional: Add custom instructions for Claude to customize its behavior for your project
|
|
56
|
+
# custom_instructions: |
|
|
57
|
+
# Follow our coding standards
|
|
58
|
+
# Ensure all new code has tests
|
|
59
|
+
# Use TypeScript for new files
|
|
60
|
+
|
|
61
|
+
# Optional: Custom environment variables for Claude
|
|
62
|
+
# claude_env: |
|
|
63
|
+
# NODE_ENV: test
|
|
64
|
+
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mkv-episode-matcher
|
|
3
|
-
Version: 0.9.
|
|
3
|
+
Version: 0.9.6
|
|
4
4
|
Summary: The MKV Episode Matcher is a tool for identifying TV series episodes from MKV files and renaming the files accordingly.
|
|
5
5
|
Home-page: https://github.com/Jsakkos/mkv-episode-matcher
|
|
6
6
|
Author: Jonathan Sakkos
|
|
@@ -43,7 +43,7 @@ def set_config(
|
|
|
43
43
|
Returns:
|
|
44
44
|
None
|
|
45
45
|
"""
|
|
46
|
-
config = configparser.ConfigParser()
|
|
46
|
+
config = configparser.ConfigParser(interpolation=None)
|
|
47
47
|
config["Config"] = {
|
|
48
48
|
"tmdb_api_key": str(tmdb_api_key),
|
|
49
49
|
"show_dir": show_dir,
|
|
@@ -56,7 +56,7 @@ def set_config(
|
|
|
56
56
|
logger.info(
|
|
57
57
|
f"Setting config with API:{tmdb_api_key}, show_dir: {show_dir}, and max_threads: {MAX_THREADS}"
|
|
58
58
|
)
|
|
59
|
-
with open(file, "w") as configfile:
|
|
59
|
+
with open(file, "w", encoding="utf-8") as configfile:
|
|
60
60
|
config.write(configfile)
|
|
61
61
|
|
|
62
62
|
|
|
@@ -72,8 +72,8 @@ def get_config(file):
|
|
|
72
72
|
|
|
73
73
|
"""
|
|
74
74
|
logger.info(f"Loading config from {file}")
|
|
75
|
-
config = configparser.ConfigParser()
|
|
75
|
+
config = configparser.ConfigParser(interpolation=None)
|
|
76
76
|
if Path(file).exists():
|
|
77
|
-
config.read(file)
|
|
77
|
+
config.read(file, encoding="utf-8")
|
|
78
78
|
return config["Config"] if "Config" in config else None
|
|
79
79
|
return {}
|
|
@@ -155,11 +155,13 @@ class EpisodeMatcher:
|
|
|
155
155
|
]
|
|
156
156
|
|
|
157
157
|
reference_files = []
|
|
158
|
-
for
|
|
158
|
+
for pattern in patterns:
|
|
159
|
+
# Use case-insensitive file extension matching by checking both .srt and .SRT
|
|
160
|
+
srt_files = list(reference_dir.glob("*.srt")) + list(reference_dir.glob("*.SRT"))
|
|
159
161
|
files = [
|
|
160
162
|
f
|
|
161
|
-
for f in
|
|
162
|
-
if
|
|
163
|
+
for f in srt_files
|
|
164
|
+
if re.search(f"{pattern}\\d+", f.name, re.IGNORECASE)
|
|
163
165
|
]
|
|
164
166
|
reference_files.extend(files)
|
|
165
167
|
|
{mkv_episode_matcher-0.9.4 → mkv_episode_matcher-0.9.6}/mkv_episode_matcher.egg-info/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mkv-episode-matcher
|
|
3
|
-
Version: 0.9.
|
|
3
|
+
Version: 0.9.6
|
|
4
4
|
Summary: The MKV Episode Matcher is a tool for identifying TV series episodes from MKV files and renaming the files accordingly.
|
|
5
5
|
Home-page: https://github.com/Jsakkos/mkv-episode-matcher
|
|
6
6
|
Author: Jonathan Sakkos
|
{mkv_episode_matcher-0.9.4 → mkv_episode_matcher-0.9.6}/mkv_episode_matcher.egg-info/SOURCES.txt
RENAMED
|
@@ -11,7 +11,10 @@ pyproject.toml
|
|
|
11
11
|
setup.cfg
|
|
12
12
|
setup.py
|
|
13
13
|
uv.lock
|
|
14
|
+
.claude/settings.local.json
|
|
14
15
|
.github/funding.yml
|
|
16
|
+
.github/workflows/claude-code-review.yml
|
|
17
|
+
.github/workflows/claude.yml
|
|
15
18
|
.github/workflows/documentation.yml
|
|
16
19
|
.github/workflows/python-publish.yml
|
|
17
20
|
.github/workflows/tests.yml
|
|
@@ -39,6 +42,8 @@ mkv_episode_matcher.egg-info/entry_points.txt
|
|
|
39
42
|
mkv_episode_matcher.egg-info/requires.txt
|
|
40
43
|
mkv_episode_matcher.egg-info/top_level.txt
|
|
41
44
|
tests/__init__.py
|
|
45
|
+
tests/test_config_special_characters.py
|
|
46
|
+
tests/test_episode_identification.py
|
|
42
47
|
tests/test_main.py
|
|
43
48
|
tests/test_path_handling.py
|
|
44
49
|
tests/test_trailing_slash.py
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[metadata]
|
|
2
2
|
name = mkv_episode_matcher
|
|
3
|
-
version = 0.9.
|
|
3
|
+
version = 0.9.6
|
|
4
4
|
author = Jonathan Sakkos
|
|
5
5
|
author_email = jonathansakkos@gmail.com
|
|
6
6
|
description = The MKV Episode Matcher is a tool for identifying TV series episodes from MKV files and renaming the files accordingly.
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"""Test cases for config.py handling of special characters in passwords."""
|
|
2
|
+
|
|
3
|
+
import tempfile
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
from mkv_episode_matcher.config import get_config, set_config
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TestConfigSpecialCharacters:
|
|
12
|
+
"""Test that config handling works correctly with special characters in passwords."""
|
|
13
|
+
|
|
14
|
+
@pytest.fixture
|
|
15
|
+
def temp_config_file(self):
|
|
16
|
+
"""Create a temporary config file for testing."""
|
|
17
|
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.ini', delete=False) as f:
|
|
18
|
+
yield f.name
|
|
19
|
+
Path(f.name).unlink(missing_ok=True)
|
|
20
|
+
|
|
21
|
+
@pytest.fixture
|
|
22
|
+
def mock_config_data(self):
|
|
23
|
+
"""Mock configuration data for testing."""
|
|
24
|
+
return {
|
|
25
|
+
"tmdb_api_key": "test_tmdb_api_key",
|
|
26
|
+
"open_subtitles_api_key": "test_os_api_key",
|
|
27
|
+
"open_subtitles_user_agent": "test_user_agent",
|
|
28
|
+
"open_subtitles_username": "test_username",
|
|
29
|
+
"show_dir": "/test/path"
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
def test_password_with_percent_symbol(self, temp_config_file, mock_config_data):
|
|
33
|
+
"""Test that passwords containing % symbol don't cause interpolation errors."""
|
|
34
|
+
password_with_percent = "H7z*X$X29JdJ^#%Q" # gitguardian:ignore
|
|
35
|
+
|
|
36
|
+
# This should not raise a ValueError
|
|
37
|
+
set_config(
|
|
38
|
+
mock_config_data["tmdb_api_key"],
|
|
39
|
+
mock_config_data["open_subtitles_api_key"],
|
|
40
|
+
mock_config_data["open_subtitles_user_agent"],
|
|
41
|
+
mock_config_data["open_subtitles_username"],
|
|
42
|
+
password_with_percent,
|
|
43
|
+
mock_config_data["show_dir"],
|
|
44
|
+
temp_config_file,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
# Verify the config was written successfully
|
|
48
|
+
config = get_config(temp_config_file)
|
|
49
|
+
assert config is not None
|
|
50
|
+
assert config["open_subtitles_password"] == password_with_percent
|
|
51
|
+
|
|
52
|
+
def test_password_with_multiple_percent_symbols(self, temp_config_file, mock_config_data):
|
|
53
|
+
"""Test that passwords with multiple % symbols work correctly."""
|
|
54
|
+
password_with_percents = "password%with%multiple%percent%signs"
|
|
55
|
+
|
|
56
|
+
set_config(
|
|
57
|
+
mock_config_data["tmdb_api_key"],
|
|
58
|
+
mock_config_data["open_subtitles_api_key"],
|
|
59
|
+
mock_config_data["open_subtitles_user_agent"],
|
|
60
|
+
mock_config_data["open_subtitles_username"],
|
|
61
|
+
password_with_percents,
|
|
62
|
+
mock_config_data["show_dir"],
|
|
63
|
+
temp_config_file,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
config = get_config(temp_config_file)
|
|
67
|
+
assert config["open_subtitles_password"] == password_with_percents
|
|
68
|
+
|
|
69
|
+
def test_password_with_interpolation_like_syntax(self, temp_config_file, mock_config_data):
|
|
70
|
+
"""Test that passwords resembling interpolation syntax are handled correctly."""
|
|
71
|
+
# This resembles ConfigParser interpolation syntax but should be treated literally
|
|
72
|
+
password_with_interpolation = "%(section)s_password_%(option)s"
|
|
73
|
+
|
|
74
|
+
set_config(
|
|
75
|
+
mock_config_data["tmdb_api_key"],
|
|
76
|
+
mock_config_data["open_subtitles_api_key"],
|
|
77
|
+
mock_config_data["open_subtitles_user_agent"],
|
|
78
|
+
mock_config_data["open_subtitles_username"],
|
|
79
|
+
password_with_interpolation,
|
|
80
|
+
mock_config_data["show_dir"],
|
|
81
|
+
temp_config_file,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
config = get_config(temp_config_file)
|
|
85
|
+
assert config["open_subtitles_password"] == password_with_interpolation
|
|
86
|
+
|
|
87
|
+
def test_password_with_various_special_characters(self, temp_config_file, mock_config_data):
|
|
88
|
+
"""Test that passwords with various special characters work correctly."""
|
|
89
|
+
special_passwords = [
|
|
90
|
+
"pass@word!123",
|
|
91
|
+
"my$ecret#key*",
|
|
92
|
+
"complex&password^with()brackets[]",
|
|
93
|
+
"unicode_测试_password",
|
|
94
|
+
"spaces in password",
|
|
95
|
+
"tabs\tand\nnewlines",
|
|
96
|
+
]
|
|
97
|
+
|
|
98
|
+
for password in special_passwords:
|
|
99
|
+
set_config(
|
|
100
|
+
mock_config_data["tmdb_api_key"],
|
|
101
|
+
mock_config_data["open_subtitles_api_key"],
|
|
102
|
+
mock_config_data["open_subtitles_user_agent"],
|
|
103
|
+
mock_config_data["open_subtitles_username"],
|
|
104
|
+
password,
|
|
105
|
+
mock_config_data["show_dir"],
|
|
106
|
+
temp_config_file,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
config = get_config(temp_config_file)
|
|
110
|
+
assert config["open_subtitles_password"] == password, f"Failed for password: {password}"
|
|
111
|
+
|
|
112
|
+
def test_original_bug_case(self, temp_config_file, mock_config_data):
|
|
113
|
+
"""Test the specific password from the original bug report."""
|
|
114
|
+
# This is the exact password that caused the original issue
|
|
115
|
+
problematic_password = "H7z*X$X29JdJ^#%Q" # gitguardian:ignore
|
|
116
|
+
|
|
117
|
+
# Before the fix, this would raise:
|
|
118
|
+
# ValueError: invalid interpolation syntax in 'H7z*X$X29JdJ^#%Q' at position 14
|
|
119
|
+
set_config(
|
|
120
|
+
mock_config_data["tmdb_api_key"],
|
|
121
|
+
mock_config_data["open_subtitles_api_key"],
|
|
122
|
+
mock_config_data["open_subtitles_user_agent"],
|
|
123
|
+
mock_config_data["open_subtitles_username"],
|
|
124
|
+
problematic_password,
|
|
125
|
+
mock_config_data["show_dir"],
|
|
126
|
+
temp_config_file,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
# Verify we can read it back correctly
|
|
130
|
+
config = get_config(temp_config_file)
|
|
131
|
+
assert config is not None
|
|
132
|
+
assert config["open_subtitles_password"] == problematic_password
|
|
133
|
+
|
|
134
|
+
# Verify all other fields are preserved
|
|
135
|
+
assert config["tmdb_api_key"] == mock_config_data["tmdb_api_key"]
|
|
136
|
+
assert config["open_subtitles_username"] == mock_config_data["open_subtitles_username"]
|
|
137
|
+
assert config["show_dir"] == mock_config_data["show_dir"]
|
|
138
|
+
|
|
139
|
+
def test_empty_password(self, temp_config_file, mock_config_data):
|
|
140
|
+
"""Test that empty passwords are handled correctly."""
|
|
141
|
+
empty_password = ""
|
|
142
|
+
|
|
143
|
+
set_config(
|
|
144
|
+
mock_config_data["tmdb_api_key"],
|
|
145
|
+
mock_config_data["open_subtitles_api_key"],
|
|
146
|
+
mock_config_data["open_subtitles_user_agent"],
|
|
147
|
+
mock_config_data["open_subtitles_username"],
|
|
148
|
+
empty_password,
|
|
149
|
+
mock_config_data["show_dir"],
|
|
150
|
+
temp_config_file,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
config = get_config(temp_config_file)
|
|
154
|
+
assert config["open_subtitles_password"] == empty_password
|
|
155
|
+
|
|
156
|
+
def test_config_persistence(self, temp_config_file, mock_config_data):
|
|
157
|
+
"""Test that config values persist correctly across multiple operations."""
|
|
158
|
+
password = "persistent%password#123"
|
|
159
|
+
|
|
160
|
+
# Set config
|
|
161
|
+
set_config(
|
|
162
|
+
mock_config_data["tmdb_api_key"],
|
|
163
|
+
mock_config_data["open_subtitles_api_key"],
|
|
164
|
+
mock_config_data["open_subtitles_user_agent"],
|
|
165
|
+
mock_config_data["open_subtitles_username"],
|
|
166
|
+
password,
|
|
167
|
+
mock_config_data["show_dir"],
|
|
168
|
+
temp_config_file,
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
# Read config multiple times to ensure consistency
|
|
172
|
+
for _ in range(3):
|
|
173
|
+
config = get_config(temp_config_file)
|
|
174
|
+
assert config["open_subtitles_password"] == password
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
import tempfile
|
|
3
|
+
import re
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from unittest.mock import Mock, patch
|
|
6
|
+
|
|
7
|
+
from mkv_episode_matcher.episode_identification import EpisodeMatcher
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TestEpisodeIdentificationPatternMatching(unittest.TestCase):
|
|
11
|
+
"""Test the pattern matching functionality in episode identification."""
|
|
12
|
+
|
|
13
|
+
def setUp(self):
|
|
14
|
+
"""Set up test fixtures."""
|
|
15
|
+
self.temp_dir = tempfile.TemporaryDirectory()
|
|
16
|
+
self.cache_dir = Path(self.temp_dir.name)
|
|
17
|
+
|
|
18
|
+
# Mock subtitle cache
|
|
19
|
+
self.mock_subtitle_cache = Mock()
|
|
20
|
+
|
|
21
|
+
# Create identifier instance
|
|
22
|
+
self.identifier = EpisodeMatcher(
|
|
23
|
+
cache_dir=self.cache_dir,
|
|
24
|
+
show_name="Test Show (2023)"
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
def tearDown(self):
|
|
28
|
+
"""Clean up test fixtures."""
|
|
29
|
+
self.temp_dir.cleanup()
|
|
30
|
+
|
|
31
|
+
def _create_test_files(self, filenames, show_name=None):
|
|
32
|
+
"""Create test subtitle files in the cache directory."""
|
|
33
|
+
if show_name is None:
|
|
34
|
+
show_name = self.identifier.show_name
|
|
35
|
+
|
|
36
|
+
show_dir = self.cache_dir / "data" / show_name
|
|
37
|
+
show_dir.mkdir(parents=True, exist_ok=True)
|
|
38
|
+
|
|
39
|
+
created_files = []
|
|
40
|
+
for filename in filenames:
|
|
41
|
+
file_path = show_dir / filename
|
|
42
|
+
file_path.write_text("dummy subtitle content")
|
|
43
|
+
created_files.append(file_path)
|
|
44
|
+
|
|
45
|
+
return created_files
|
|
46
|
+
|
|
47
|
+
def test_pattern_matching_s01e_format(self):
|
|
48
|
+
"""Test pattern matching for S01E format files."""
|
|
49
|
+
# Create test files with S01E format
|
|
50
|
+
test_files = [
|
|
51
|
+
"Test Show (2023) - S01E01.srt",
|
|
52
|
+
"Test Show (2023) - S01E02.srt",
|
|
53
|
+
"Test Show (2023) - S01E10.srt",
|
|
54
|
+
"Test Show (2023) - S02E01.srt", # Should not match season 1
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
self._create_test_files(test_files)
|
|
58
|
+
|
|
59
|
+
# Test season 1 matching
|
|
60
|
+
reference_files = self.identifier.get_reference_files(1)
|
|
61
|
+
|
|
62
|
+
# Should find 3 files for season 1
|
|
63
|
+
self.assertEqual(len(reference_files), 3)
|
|
64
|
+
|
|
65
|
+
# Verify correct files are matched
|
|
66
|
+
matched_names = [f.name for f in reference_files]
|
|
67
|
+
self.assertIn("Test Show (2023) - S01E01.srt", matched_names)
|
|
68
|
+
self.assertIn("Test Show (2023) - S01E02.srt", matched_names)
|
|
69
|
+
self.assertIn("Test Show (2023) - S01E10.srt", matched_names)
|
|
70
|
+
self.assertNotIn("Test Show (2023) - S02E01.srt", matched_names)
|
|
71
|
+
|
|
72
|
+
def test_pattern_matching_s1e_format(self):
|
|
73
|
+
"""Test pattern matching for S1E format files."""
|
|
74
|
+
test_files = [
|
|
75
|
+
"Show - S1E01.srt",
|
|
76
|
+
"Show - S1E05.srt",
|
|
77
|
+
"Show - S2E01.srt", # Should not match season 1
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
self._create_test_files(test_files)
|
|
81
|
+
|
|
82
|
+
reference_files = self.identifier.get_reference_files(1)
|
|
83
|
+
|
|
84
|
+
# Should find 2 files for season 1
|
|
85
|
+
self.assertEqual(len(reference_files), 2)
|
|
86
|
+
|
|
87
|
+
matched_names = [f.name for f in reference_files]
|
|
88
|
+
self.assertIn("Show - S1E01.srt", matched_names)
|
|
89
|
+
self.assertIn("Show - S1E05.srt", matched_names)
|
|
90
|
+
self.assertNotIn("Show - S2E01.srt", matched_names)
|
|
91
|
+
|
|
92
|
+
def test_pattern_matching_01x_format(self):
|
|
93
|
+
"""Test pattern matching for 01x format files."""
|
|
94
|
+
test_files = [
|
|
95
|
+
"Show - 01x01.srt",
|
|
96
|
+
"Show - 01x15.srt",
|
|
97
|
+
"Show - 02x01.srt", # Should not match season 1
|
|
98
|
+
]
|
|
99
|
+
|
|
100
|
+
self._create_test_files(test_files)
|
|
101
|
+
|
|
102
|
+
reference_files = self.identifier.get_reference_files(1)
|
|
103
|
+
|
|
104
|
+
# Should find 2 files for season 1
|
|
105
|
+
self.assertEqual(len(reference_files), 2)
|
|
106
|
+
|
|
107
|
+
matched_names = [f.name for f in reference_files]
|
|
108
|
+
self.assertIn("Show - 01x01.srt", matched_names)
|
|
109
|
+
self.assertIn("Show - 01x15.srt", matched_names)
|
|
110
|
+
self.assertNotIn("Show - 02x01.srt", matched_names)
|
|
111
|
+
|
|
112
|
+
def test_pattern_matching_1x_format(self):
|
|
113
|
+
"""Test pattern matching for 1x format files."""
|
|
114
|
+
test_files = [
|
|
115
|
+
"Show - 1x01.srt",
|
|
116
|
+
"Show - 1x08.srt",
|
|
117
|
+
"Show - 2x01.srt", # Should not match season 1
|
|
118
|
+
]
|
|
119
|
+
|
|
120
|
+
self._create_test_files(test_files)
|
|
121
|
+
|
|
122
|
+
reference_files = self.identifier.get_reference_files(1)
|
|
123
|
+
|
|
124
|
+
# Should find 2 files for season 1
|
|
125
|
+
self.assertEqual(len(reference_files), 2)
|
|
126
|
+
|
|
127
|
+
matched_names = [f.name for f in reference_files]
|
|
128
|
+
self.assertIn("Show - 1x01.srt", matched_names)
|
|
129
|
+
self.assertIn("Show - 1x08.srt", matched_names)
|
|
130
|
+
self.assertNotIn("Show - 2x01.srt", matched_names)
|
|
131
|
+
|
|
132
|
+
def test_pattern_matching_mixed_formats(self):
|
|
133
|
+
"""Test pattern matching with mixed file formats."""
|
|
134
|
+
test_files = [
|
|
135
|
+
"Show - S01E01.srt",
|
|
136
|
+
"Show - S1E02.srt",
|
|
137
|
+
"Show - 01x03.srt",
|
|
138
|
+
"Show - 1x04.srt",
|
|
139
|
+
"Show - S02E01.srt", # Different season
|
|
140
|
+
"Show - episode1.srt", # No matching pattern
|
|
141
|
+
]
|
|
142
|
+
|
|
143
|
+
self._create_test_files(test_files)
|
|
144
|
+
|
|
145
|
+
reference_files = self.identifier.get_reference_files(1)
|
|
146
|
+
|
|
147
|
+
# Should find 4 files for season 1 (all matching patterns)
|
|
148
|
+
self.assertEqual(len(reference_files), 4)
|
|
149
|
+
|
|
150
|
+
matched_names = [f.name for f in reference_files]
|
|
151
|
+
self.assertIn("Show - S01E01.srt", matched_names)
|
|
152
|
+
self.assertIn("Show - S1E02.srt", matched_names)
|
|
153
|
+
self.assertIn("Show - 01x03.srt", matched_names)
|
|
154
|
+
self.assertIn("Show - 1x04.srt", matched_names)
|
|
155
|
+
self.assertNotIn("Show - S02E01.srt", matched_names)
|
|
156
|
+
self.assertNotIn("Show - episode1.srt", matched_names)
|
|
157
|
+
|
|
158
|
+
def test_case_insensitive_matching(self):
|
|
159
|
+
"""Test that pattern matching is case insensitive."""
|
|
160
|
+
test_files = [
|
|
161
|
+
"show - s01e01.srt", # lowercase s and e
|
|
162
|
+
"SHOW - S01E02.SRT", # uppercase everything
|
|
163
|
+
"Show - S01e03.srt", # mixed case
|
|
164
|
+
]
|
|
165
|
+
|
|
166
|
+
self._create_test_files(test_files)
|
|
167
|
+
|
|
168
|
+
reference_files = self.identifier.get_reference_files(1)
|
|
169
|
+
|
|
170
|
+
# Should find all 3 files regardless of case
|
|
171
|
+
self.assertEqual(len(reference_files), 3)
|
|
172
|
+
|
|
173
|
+
def test_dexter_new_blood_issue_reproduction(self):
|
|
174
|
+
"""Test the specific issue from bug report with Dexter New Blood files."""
|
|
175
|
+
# Create files matching the exact pattern from the bug report
|
|
176
|
+
test_files = [
|
|
177
|
+
"Dexter - New Blood (2021) - S01E01.srt",
|
|
178
|
+
"Dexter - New Blood (2021) - S01E02.srt",
|
|
179
|
+
"Dexter - New Blood (2021) - S01E03.srt",
|
|
180
|
+
"Dexter - New Blood (2021) - S01E04.srt",
|
|
181
|
+
"Dexter - New Blood (2021) - S01E05.srt",
|
|
182
|
+
"Dexter - New Blood (2021) - S01E06.srt",
|
|
183
|
+
"Dexter - New Blood (2021) - S01E07.srt",
|
|
184
|
+
"Dexter - New Blood (2021) - S01E08.srt",
|
|
185
|
+
"Dexter - New Blood (2021) - S01E09.srt",
|
|
186
|
+
"Dexter - New Blood (2021) - S01E10.srt",
|
|
187
|
+
]
|
|
188
|
+
|
|
189
|
+
# Update the identifier to use the exact show name from the bug report
|
|
190
|
+
show_name = "Dexter - New Blood (2021)"
|
|
191
|
+
self.identifier.show_name = show_name
|
|
192
|
+
|
|
193
|
+
self._create_test_files(test_files, show_name)
|
|
194
|
+
|
|
195
|
+
reference_files = self.identifier.get_reference_files(1)
|
|
196
|
+
|
|
197
|
+
# Should find all 10 files - this was the bug that was failing
|
|
198
|
+
self.assertEqual(len(reference_files), 10,
|
|
199
|
+
f"Expected 10 files but found {len(reference_files)}. "
|
|
200
|
+
f"Files found: {[f.name for f in reference_files]}")
|
|
201
|
+
|
|
202
|
+
def test_no_duplicate_files(self):
|
|
203
|
+
"""Test that duplicate matches are removed properly."""
|
|
204
|
+
# Create a file that could match multiple patterns
|
|
205
|
+
test_files = [
|
|
206
|
+
"Show - S01E01.srt", # Could match both S01E and 01x patterns if logic is wrong
|
|
207
|
+
]
|
|
208
|
+
|
|
209
|
+
self._create_test_files(test_files)
|
|
210
|
+
|
|
211
|
+
reference_files = self.identifier.get_reference_files(1)
|
|
212
|
+
|
|
213
|
+
# Should find only 1 unique file, not duplicates
|
|
214
|
+
self.assertEqual(len(reference_files), 1)
|
|
215
|
+
|
|
216
|
+
# Verify it's the correct file
|
|
217
|
+
self.assertEqual(reference_files[0].name, "Show - S01E01.srt")
|
|
218
|
+
|
|
219
|
+
def test_empty_directory(self):
|
|
220
|
+
"""Test behavior when no matching files exist."""
|
|
221
|
+
# Create the directory but no files
|
|
222
|
+
show_dir = self.cache_dir / "data" / "Test Show (2023)"
|
|
223
|
+
show_dir.mkdir(parents=True, exist_ok=True)
|
|
224
|
+
|
|
225
|
+
reference_files = self.identifier.get_reference_files(1)
|
|
226
|
+
|
|
227
|
+
# Should find no files
|
|
228
|
+
self.assertEqual(len(reference_files), 0)
|
|
229
|
+
|
|
230
|
+
def test_directory_does_not_exist(self):
|
|
231
|
+
"""Test behavior when the show directory doesn't exist."""
|
|
232
|
+
# Don't create the directory
|
|
233
|
+
reference_files = self.identifier.get_reference_files(1)
|
|
234
|
+
|
|
235
|
+
# Should find no files (glob returns empty on non-existent directory)
|
|
236
|
+
self.assertEqual(len(reference_files), 0)
|
|
237
|
+
|
|
238
|
+
def test_caching_functionality(self):
|
|
239
|
+
"""Test that results are properly cached."""
|
|
240
|
+
test_files = ["Show - S01E01.srt"]
|
|
241
|
+
self._create_test_files(test_files)
|
|
242
|
+
|
|
243
|
+
# First call
|
|
244
|
+
reference_files_1 = self.identifier.get_reference_files(1)
|
|
245
|
+
|
|
246
|
+
# Second call should return cached result
|
|
247
|
+
reference_files_2 = self.identifier.get_reference_files(1)
|
|
248
|
+
|
|
249
|
+
# Should be the same result
|
|
250
|
+
self.assertEqual(reference_files_1, reference_files_2)
|
|
251
|
+
|
|
252
|
+
# Verify it's actually using the cache by checking the cache directly
|
|
253
|
+
cache_key = ("Test Show (2023)", 1)
|
|
254
|
+
self.assertIn(cache_key, self.identifier.reference_files_cache)
|
|
255
|
+
|
|
256
|
+
def test_different_seasons(self):
|
|
257
|
+
"""Test that different seasons are handled correctly."""
|
|
258
|
+
test_files = [
|
|
259
|
+
"Show - S01E01.srt",
|
|
260
|
+
"Show - S01E02.srt",
|
|
261
|
+
"Show - S02E01.srt",
|
|
262
|
+
"Show - S02E02.srt",
|
|
263
|
+
"Show - S03E01.srt",
|
|
264
|
+
]
|
|
265
|
+
|
|
266
|
+
self._create_test_files(test_files)
|
|
267
|
+
|
|
268
|
+
# Test season 1
|
|
269
|
+
season1_files = self.identifier.get_reference_files(1)
|
|
270
|
+
self.assertEqual(len(season1_files), 2)
|
|
271
|
+
|
|
272
|
+
# Test season 2
|
|
273
|
+
season2_files = self.identifier.get_reference_files(2)
|
|
274
|
+
self.assertEqual(len(season2_files), 2)
|
|
275
|
+
|
|
276
|
+
# Test season 3
|
|
277
|
+
season3_files = self.identifier.get_reference_files(3)
|
|
278
|
+
self.assertEqual(len(season3_files), 1)
|
|
279
|
+
|
|
280
|
+
# Test non-existent season
|
|
281
|
+
season4_files = self.identifier.get_reference_files(4)
|
|
282
|
+
self.assertEqual(len(season4_files), 0)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
if __name__ == '__main__':
|
|
286
|
+
unittest.main()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mkv_episode_matcher-0.9.4 → mkv_episode_matcher-0.9.6}/.github/workflows/python-publish.yml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mkv_episode_matcher-0.9.4 → mkv_episode_matcher-0.9.6}/mkv_episode_matcher/episode_matcher.py
RENAMED
|
File without changes
|
{mkv_episode_matcher-0.9.4 → mkv_episode_matcher-0.9.6}/mkv_episode_matcher/subtitle_utils.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mkv_episode_matcher-0.9.4 → mkv_episode_matcher-0.9.6}/mkv_episode_matcher.egg-info/requires.txt
RENAMED
|
File without changes
|
{mkv_episode_matcher-0.9.4 → mkv_episode_matcher-0.9.6}/mkv_episode_matcher.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|