donghua-cli 3.2.0__tar.gz → 3.2.1__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.
Files changed (60) hide show
  1. {donghua_cli-3.2.0 → donghua_cli-3.2.1}/.github/workflows/publish.yml +4 -1
  2. {donghua_cli-3.2.0 → donghua_cli-3.2.1}/CHANGELOG.md +6 -0
  3. {donghua_cli-3.2.0 → donghua_cli-3.2.1}/PKG-INFO +1 -1
  4. {donghua_cli-3.2.0 → donghua_cli-3.2.1}/packaging/aur/PKGBUILD +1 -1
  5. {donghua_cli-3.2.0 → donghua_cli-3.2.1}/packaging/homebrew/donghua-cli.rb +2 -2
  6. {donghua_cli-3.2.0 → donghua_cli-3.2.1}/pyproject.toml +1 -1
  7. {donghua_cli-3.2.0 → donghua_cli-3.2.1}/scripts/release-prep.sh +25 -11
  8. {donghua_cli-3.2.0 → donghua_cli-3.2.1}/src/donghua_cli/__init__.py +1 -1
  9. {donghua_cli-3.2.0 → donghua_cli-3.2.1}/src/donghua_cli/extractor.py +17 -5
  10. {donghua_cli-3.2.0 → donghua_cli-3.2.1}/src/donghua_cli/player.py +19 -7
  11. {donghua_cli-3.2.0 → donghua_cli-3.2.1}/src/donghua_cli/tui.py +33 -0
  12. {donghua_cli-3.2.0 → donghua_cli-3.2.1}/tests/test_extractor.py +13 -0
  13. donghua_cli-3.2.1/tests/test_player.py +96 -0
  14. {donghua_cli-3.2.0 → donghua_cli-3.2.1}/.github/workflows/ci.yml +0 -0
  15. {donghua_cli-3.2.0 → donghua_cli-3.2.1}/.gitignore +0 -0
  16. {donghua_cli-3.2.0 → donghua_cli-3.2.1}/LICENSE +0 -0
  17. {donghua_cli-3.2.0 → donghua_cli-3.2.1}/README.md +0 -0
  18. {donghua_cli-3.2.0 → donghua_cli-3.2.1}/docs/assets/banner.png +0 -0
  19. {donghua_cli-3.2.0 → donghua_cli-3.2.1}/docs/assets/banner.svg +0 -0
  20. {donghua_cli-3.2.0 → donghua_cli-3.2.1}/docs/assets/banner.txt +0 -0
  21. {donghua_cli-3.2.0 → donghua_cli-3.2.1}/docs/assets/episode-selection.png +0 -0
  22. {donghua_cli-3.2.0 → donghua_cli-3.2.1}/docs/assets/episode-selection.svg +0 -0
  23. {donghua_cli-3.2.0 → donghua_cli-3.2.1}/docs/assets/features.txt +0 -0
  24. {donghua_cli-3.2.0 → donghua_cli-3.2.1}/docs/assets/playback-controls.png +0 -0
  25. {donghua_cli-3.2.0 → donghua_cli-3.2.1}/docs/assets/playback-controls.svg +0 -0
  26. {donghua_cli-3.2.0 → donghua_cli-3.2.1}/docs/assets/social-banner.png +0 -0
  27. {donghua_cli-3.2.0 → donghua_cli-3.2.1}/docs/assets/social-banner.svg +0 -0
  28. {donghua_cli-3.2.0 → donghua_cli-3.2.1}/docs/future_ideas.md +0 -0
  29. {donghua_cli-3.2.0 → donghua_cli-3.2.1}/docs/index.html +0 -0
  30. {donghua_cli-3.2.0 → donghua_cli-3.2.1}/packaging/desktop/donghua-cli.desktop +0 -0
  31. {donghua_cli-3.2.0 → donghua_cli-3.2.1}/packaging/desktop/donghua.bat +0 -0
  32. {donghua_cli-3.2.0 → donghua_cli-3.2.1}/packaging/desktop/install-desktop.sh +0 -0
  33. {donghua_cli-3.2.0 → donghua_cli-3.2.1}/packaging/pyinstaller/donghua.spec +0 -0
  34. {donghua_cli-3.2.0 → donghua_cli-3.2.1}/packaging/scoop/donghua-cli.json +0 -0
  35. {donghua_cli-3.2.0 → donghua_cli-3.2.1}/scripts/install.ps1 +0 -0
  36. {donghua_cli-3.2.0 → donghua_cli-3.2.1}/scripts/install.sh +0 -0
  37. {donghua_cli-3.2.0 → donghua_cli-3.2.1}/src/donghua_cli/__main__.py +0 -0
  38. {donghua_cli-3.2.0 → donghua_cli-3.2.1}/src/donghua_cli/app.py +0 -0
  39. {donghua_cli-3.2.0 → donghua_cli-3.2.1}/src/donghua_cli/cache.py +0 -0
  40. {donghua_cli-3.2.0 → donghua_cli-3.2.1}/src/donghua_cli/cli.py +0 -0
  41. {donghua_cli-3.2.0 → donghua_cli-3.2.1}/src/donghua_cli/config.py +0 -0
  42. {donghua_cli-3.2.0 → donghua_cli-3.2.1}/src/donghua_cli/health.py +0 -0
  43. {donghua_cli-3.2.0 → donghua_cli-3.2.1}/src/donghua_cli/library.py +0 -0
  44. {donghua_cli-3.2.0 → donghua_cli-3.2.1}/src/donghua_cli/palette.py +0 -0
  45. {donghua_cli-3.2.0 → donghua_cli-3.2.1}/src/donghua_cli/scraper.py +0 -0
  46. {donghua_cli-3.2.0 → donghua_cli-3.2.1}/src/donghua_cli/sources/__init__.py +0 -0
  47. {donghua_cli-3.2.0 → donghua_cli-3.2.1}/src/donghua_cli/sources/animexin.py +0 -0
  48. {donghua_cli-3.2.0 → donghua_cli-3.2.1}/src/donghua_cli/sources/base.py +0 -0
  49. {donghua_cli-3.2.0 → donghua_cli-3.2.1}/src/donghua_cli/sources/hdonghua.py +0 -0
  50. {donghua_cli-3.2.0 → donghua_cli-3.2.1}/src/donghua_cli/sources/lmanime.py +0 -0
  51. {donghua_cli-3.2.0 → donghua_cli-3.2.1}/src/donghua_cli/sources/luciferdonghua.py +0 -0
  52. {donghua_cli-3.2.0 → donghua_cli-3.2.1}/src/donghua_cli/sources/misterdonghua.py +0 -0
  53. {donghua_cli-3.2.0 → donghua_cli-3.2.1}/src/donghua_cli/theme.py +0 -0
  54. {donghua_cli-3.2.0 → donghua_cli-3.2.1}/src/donghua_cli/ui.py +0 -0
  55. {donghua_cli-3.2.0 → donghua_cli-3.2.1}/src/donghua_cli/utils.py +0 -0
  56. {donghua_cli-3.2.0 → donghua_cli-3.2.1}/tests/__init__.py +0 -0
  57. {donghua_cli-3.2.0 → donghua_cli-3.2.1}/tests/test_library.py +0 -0
  58. {donghua_cli-3.2.0 → donghua_cli-3.2.1}/tests/test_scraper.py +0 -0
  59. {donghua_cli-3.2.0 → donghua_cli-3.2.1}/tests/test_sources.py +0 -0
  60. {donghua_cli-3.2.0 → donghua_cli-3.2.1}/tests/test_utils.py +0 -0
@@ -30,7 +30,10 @@ jobs:
30
30
  env:
31
31
  TWINE_USERNAME: __token__
32
32
  TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}
33
- run: twine upload dist/*
33
+ # --skip-existing makes re-runs idempotent: if a tag is re-pushed
34
+ # to recover from a downstream failure, twine treats "file already
35
+ # exists" as success instead of failing the whole pipeline.
36
+ run: twine upload --skip-existing dist/*
34
37
 
35
38
  - name: Bump AUR / Homebrew / Scoop formulas
36
39
  # Runs after PyPI upload so the sdist hash matches what users install.
@@ -2,6 +2,12 @@
2
2
 
3
3
  All notable changes to Donghua CLI will be documented in this file.
4
4
 
5
+ ## [3.2.1] - 2026-06-22
6
+
7
+ ### Fixed
8
+ - **mpv process leak on auto-next.** The TUI kept no handle on the running player, so Next / Replay / auto-next launched a fresh mpv without stopping the previous one — leaking processes, audio, and IPC sockets, and stranding `wait_for_end` threads. The playback screen now holds the active player, stops it before each new playback, tears it down on screen exit, and uses a per-playback generation tag so a superseded worker never fires auto-next. `wait_for_end` binds the process locally so a concurrent `stop()` can't crash its poll loop.
9
+ - **`geo.dailymotion.com` embeds played as HTML and died in <1s.** Canonicalization only handled the legacy `/embed/video/<id>` path; modern aggregators hand over `geo.dailymotion.com/player/<pid>.html?video=<id>` (id in a query param, no `/video/` in the path). Every embed/geo/player form now collapses to the canonical `https://www.dailymotion.com/video/<id>`.
10
+
5
11
  ## [3.2.0] - 2026-05-21
6
12
 
7
13
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: donghua-cli
3
- Version: 3.2.0
3
+ Version: 3.2.1
4
4
  Summary: Wuxia-themed terminal client for streaming Chinese animation
5
5
  License-Expression: MIT
6
6
  License-File: LICENSE
@@ -15,7 +15,7 @@ optdepends=(
15
15
  'ffmpeg: required by yt-dlp for merging streams'
16
16
  )
17
17
  source=("https://files.pythonhosted.org/packages/source/d/${pkgname}/${pkgname//-/_}-${pkgver}.tar.gz")
18
- sha256sums=('UPDATE_WITH_ACTUAL_SHA256')
18
+ sha256sums=('fc6f2a9d1fb1542ca8f616963fdf18ef49cb363982feece6989aa432f4d0b9c8')
19
19
 
20
20
  build() {
21
21
  cd "${pkgname//-/_}-${pkgver}"
@@ -4,8 +4,8 @@ class DonghuaCli < Formula
4
4
  desc "Wuxia-themed terminal client for streaming Chinese animation"
5
5
  homepage "https://github.com/Thanukamax/donghua-cli"
6
6
  # Update URL and sha256 on each release
7
- url "https://files.pythonhosted.org/packages/source/d/donghua-cli/donghua_cli-3.2.0.tar.gz"
8
- sha256 "UPDATE_WITH_ACTUAL_SHA256"
7
+ url "https://files.pythonhosted.org/packages/21/2d/3ca89831268b8c9348f3c95fee40e5ddd5d3e3584306d60966802bd9d7ee/donghua_cli-3.2.0.tar.gz"
8
+ sha256 "fc6f2a9d1fb1542ca8f616963fdf18ef49cb363982feece6989aa432f4d0b9c8"
9
9
  license "MIT"
10
10
 
11
11
  depends_on "python@3.12"
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "donghua-cli"
7
- version = "3.2.0"
7
+ version = "3.2.1"
8
8
  description = "Wuxia-themed terminal client for streaming Chinese animation"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -12,28 +12,42 @@ set -euo pipefail
12
12
  cd "$(dirname "$0")/.."
13
13
 
14
14
  VERSION="${1:-$(python3 -c 'import re,sys; print(re.search(r"\"([^\"]+)\"", open("src/donghua_cli/__init__.py").read()).group(1))')}"
15
- SDIST_URL="https://files.pythonhosted.org/packages/source/d/donghua-cli/donghua_cli-${VERSION}.tar.gz"
16
15
 
17
- echo "Fetching sdist for v${VERSION}..."
18
- tmp="$(mktemp)"
19
- trap 'rm -f "$tmp"' EXIT
16
+ # PyPI no longer reliably serves files at the legacy /packages/source/d/<pkg>/
17
+ # path from every CDN edge; use the JSON API to discover the canonical sdist
18
+ # URL + sha256 directly. Propagation can lag a few seconds after twine upload.
19
+ echo "Looking up PyPI metadata for v${VERSION}..."
20
20
 
21
- # PyPI propagation can lag a few seconds after twine upload returns.
22
- for i in 1 2 3 4 5 6; do
23
- if curl -sfL "$SDIST_URL" -o "$tmp"; then
21
+ JSON_URL="https://pypi.org/pypi/donghua-cli/${VERSION}/json"
22
+ SDIST_URL=""
23
+ SHA=""
24
+
25
+ for i in 1 2 3 4 5 6 7 8; do
26
+ METADATA="$(curl -sf "$JSON_URL" || true)"
27
+ if [[ -n "$METADATA" ]]; then
28
+ read -r SDIST_URL SHA <<<"$(printf '%s' "$METADATA" | python3 -c '
29
+ import json, sys
30
+ d = json.load(sys.stdin)
31
+ for u in d.get("urls", []):
32
+ if u.get("packagetype") == "sdist":
33
+ print(u["url"], u["digests"]["sha256"])
24
34
  break
35
+ ')"
36
+ if [[ -n "$SDIST_URL" && -n "$SHA" ]]; then
37
+ break
38
+ fi
25
39
  fi
26
40
  echo " waiting for PyPI (attempt $i)..."
27
41
  sleep 5
28
42
  done
29
43
 
30
- if [[ ! -s "$tmp" ]]; then
31
- echo "ERROR: couldn't fetch $SDIST_URL after retries." >&2
44
+ if [[ -z "$SDIST_URL" || -z "$SHA" ]]; then
45
+ echo "ERROR: couldn't find sdist for donghua-cli ${VERSION} on PyPI after retries." >&2
32
46
  exit 1
33
47
  fi
34
48
 
35
- SHA=$(sha256sum "$tmp" | cut -d' ' -f1)
36
- echo "sha256: $SHA"
49
+ echo " sdist: $SDIST_URL"
50
+ echo " sha256: $SHA"
37
51
 
38
52
  # AUR PKGBUILD
39
53
  sed -i "s/^pkgver=.*/pkgver=${VERSION}/" packaging/aur/PKGBUILD
@@ -1,3 +1,3 @@
1
1
  """Donghua CLI - Wuxia-themed terminal client for streaming Chinese animation."""
2
2
 
3
- __version__ = "3.2.0"
3
+ __version__ = "3.2.1"
@@ -70,11 +70,23 @@ def _canonicalize(url: str) -> str:
70
70
 
71
71
  url = _normalise_scheme(url)
72
72
 
73
- # Dailymotion: /embed/video/<id>[?…] → /video/<id>
74
- if "dailymotion.com/embed/video/" in url:
75
- url = url.replace("/embed/video/", "/video/")
76
- if "?" in url:
77
- url = url.split("?", 1)[0]
73
+ # Dailymotion: collapse EVERY embed/geo/player form to the canonical
74
+ # https://www.dailymotion.com/video/<id> that yt-dlp's extractor pins. The
75
+ # legacy /embed/video/<id> path is easy, but modern aggregators hand over the
76
+ # geo player form geo.dailymotion.com/player/<pid>.html?video=<id> — there's
77
+ # no /video/ in the path, so the old substring check missed it and mpv tried
78
+ # to demux the player HTML and died in <1s.
79
+ if "dailymotion." in url:
80
+ vid = None
81
+ m = re.search(r"[?&]video=([A-Za-z0-9]+)", url) # player.html?video=<id>
82
+ if m:
83
+ vid = m.group(1)
84
+ else:
85
+ m = re.search(r"dailymotion\.com/(?:embed/)?video/([A-Za-z0-9]+)", url)
86
+ if m:
87
+ vid = m.group(1)
88
+ if vid:
89
+ url = f"https://www.dailymotion.com/video/{vid}"
78
90
 
79
91
  # ok.ru: /videoembed/<id> → /video/<id>
80
92
  if "ok.ru/videoembed/" in url:
@@ -51,15 +51,15 @@ class Player:
51
51
  user-quit). Returns False if there was no live player to begin with.
52
52
  Safe to call repeatedly.
53
53
  """
54
- if self._process is None:
54
+ # Bind the process locally: a concurrent stop() (e.g. the user pressed
55
+ # Next before this episode ended) sets self._process = None, which would
56
+ # otherwise NoneType-crash the poll loop here.
57
+ proc = self._process
58
+ if proc is None:
55
59
  return False
56
- while self._process.poll() is None:
60
+ while proc.poll() is None:
57
61
  time.sleep(poll_interval)
58
- if self._ipc_path and os.path.exists(self._ipc_path):
59
- try:
60
- os.unlink(self._ipc_path)
61
- except OSError:
62
- pass
62
+ self._cleanup_socket()
63
63
  return True
64
64
 
65
65
  def stop(self) -> None:
@@ -73,6 +73,18 @@ class Player:
73
73
  except OSError:
74
74
  pass
75
75
  self._process = None
76
+ # Reap the IPC socket too — without this, every superseded player leaks
77
+ # a stale socket file in tmp until reboot.
78
+ self._cleanup_socket()
79
+
80
+ def _cleanup_socket(self) -> None:
81
+ """Remove the IPC socket file if it's still on disk. Idempotent."""
82
+ if self._ipc_path and os.path.exists(self._ipc_path):
83
+ try:
84
+ os.unlink(self._ipc_path)
85
+ except OSError:
86
+ pass
87
+ self._ipc_path = None
76
88
 
77
89
  def is_playing(self) -> bool:
78
90
  return self._process is not None and self._process.poll() is None
@@ -12,6 +12,7 @@ Screens:
12
12
  from __future__ import annotations
13
13
 
14
14
  import random
15
+ import threading
15
16
  from typing import TYPE_CHECKING
16
17
 
17
18
  from textual import on, work
@@ -35,6 +36,7 @@ from donghua_cli.palette import GLYPH, PALETTE
35
36
  from donghua_cli.theme import TECHNIQUES, level_for_episode
36
37
 
37
38
  if TYPE_CHECKING:
39
+ from donghua_cli.player import Player
38
40
  from donghua_cli.sources.base import Episode, Series
39
41
 
40
42
  # Banner technique is pinned for the lifetime of the app — never reshuffles
@@ -798,6 +800,12 @@ class PlaybackScreen(Screen):
798
800
  self._series_urls = dict(series.urls)
799
801
  self._stream_url = ""
800
802
  self._server_name = ""
803
+ # The currently-live player, so a new playback (Next / Replay / auto-next)
804
+ # can tear the old mpv down instead of leaking it. _play_gen tags each
805
+ # playback; a superseded worker checks it and bows out without advancing.
806
+ self._active_player: "Player | None" = None
807
+ self._play_gen = 0
808
+ self._play_lock = threading.Lock()
801
809
 
802
810
  def compose(self) -> ComposeResult:
803
811
  with Container(id="np-outer"):
@@ -910,7 +918,17 @@ class PlaybackScreen(Screen):
910
918
  from donghua_cli.player import Player
911
919
  from donghua_cli.sources.base import Series
912
920
 
921
+ # Tear down whatever was playing before starting the next one, and tag
922
+ # this playback. Without this, pressing Next/Replay while mpv is still
923
+ # open spawns a second mpv and strands the old wait_for_end thread.
913
924
  player = Player(self._core._quality)
925
+ with self._play_lock:
926
+ previous = self._active_player
927
+ self._play_gen += 1
928
+ gen = self._play_gen
929
+ self._active_player = player
930
+ if previous is not None:
931
+ previous.stop()
914
932
  title = f"{self._series_title} - Episode {ep.number}"
915
933
 
916
934
  if player.play(stream_url, title=title):
@@ -943,6 +961,11 @@ class PlaybackScreen(Screen):
943
961
  import time as _time
944
962
  started_playback = _time.time()
945
963
  player.wait_for_end()
964
+ # If a newer playback superseded us (user hit Next, or another
965
+ # auto-next already fired), our stop() woke wait_for_end early —
966
+ # don't advance or report failure on this stale worker.
967
+ if gen != self._play_gen:
968
+ return
946
969
  elapsed = _time.time() - started_playback
947
970
  if elapsed < 30:
948
971
  self.app.call_from_thread(
@@ -979,6 +1002,16 @@ class PlaybackScreen(Screen):
979
1002
  self.current_idx += 1
980
1003
  self._play_current()
981
1004
 
1005
+ def on_unmount(self) -> None:
1006
+ # Leaving playback (Quit / back) must kill the live mpv + its socket,
1007
+ # otherwise it keeps playing detached after the TUI screen is gone.
1008
+ with self._play_lock:
1009
+ self._play_gen += 1 # invalidate any in-flight wait_for_end worker
1010
+ player = self._active_player
1011
+ self._active_player = None
1012
+ if player is not None:
1013
+ player.stop()
1014
+
982
1015
  def action_next_ep(self) -> None:
983
1016
  if self.current_idx < len(self._episodes) - 1:
984
1017
  self.current_idx += 1
@@ -18,6 +18,19 @@ class TestCanonicalize:
18
18
  url = "https://www.dailymotion.com/video/k3fOeWYJYDH5DVybIIp"
19
19
  assert _canonicalize(url) == url
20
20
 
21
+ def test_dailymotion_geo_player_html_collapses_to_video(self):
22
+ # Modern aggregator embed: no /video/ in the path, id is a query param.
23
+ url = "https://geo.dailymotion.com/player/xabcd.html?video=k5abc123&mute=true"
24
+ assert _canonicalize(url) == "https://www.dailymotion.com/video/k5abc123"
25
+
26
+ def test_dailymotion_geo_embed_video_collapses_to_www(self):
27
+ url = "https://geo.dailymotion.com/embed/video/k9zzz?autoplay=1"
28
+ assert _canonicalize(url) == "https://www.dailymotion.com/video/k9zzz"
29
+
30
+ def test_dailymotion_protocol_relative_geo_player(self):
31
+ url = "//geo.dailymotion.com/player/x1234.html?video=kPqR9"
32
+ assert _canonicalize(url) == "https://www.dailymotion.com/video/kPqR9"
33
+
21
34
  def test_ok_ru_embed_becomes_https_video(self):
22
35
  assert _canonicalize("//ok.ru/videoembed/6816742902324") == "https://ok.ru/video/6816742902324"
23
36
 
@@ -0,0 +1,96 @@
1
+ """Tests for the Player lifecycle — process + IPC socket teardown.
2
+
3
+ These cover the v3.2.1 auto-next leak fix: a superseded player must release its
4
+ mpv process and IPC socket, and wait_for_end must survive a concurrent stop()
5
+ that nulls the process reference (which is what Next / Replay / auto-next do).
6
+ """
7
+
8
+ import os
9
+ import tempfile
10
+ import threading
11
+ import time
12
+
13
+ from donghua_cli.player import Player
14
+
15
+
16
+ class _FakeProc:
17
+ """Stand-in for subprocess.Popen: stays alive for `alive_polls` poll() calls,
18
+ then reports the exit code set by terminate()/kill() (or 0)."""
19
+
20
+ def __init__(self, alive_polls: int = 0):
21
+ self._remaining = alive_polls
22
+ self._code: int | None = None
23
+ self.terminated = False
24
+ self.killed = False
25
+
26
+ def poll(self):
27
+ if self._remaining > 0:
28
+ self._remaining -= 1
29
+ return None
30
+ return self._code if self._code is not None else 0
31
+
32
+ def terminate(self):
33
+ self.terminated = True
34
+ self._code = -15
35
+
36
+ def kill(self):
37
+ self.killed = True
38
+ self._code = -9
39
+
40
+ def wait(self, timeout=None):
41
+ return self._code or 0
42
+
43
+
44
+ def _make_socket_file() -> str:
45
+ fd, path = tempfile.mkstemp(prefix="dhua-test-", suffix=".sock")
46
+ os.close(fd)
47
+ return path
48
+
49
+
50
+ class TestPlayerTeardown:
51
+ def test_wait_for_end_no_process_returns_false(self):
52
+ assert Player(quality="720").wait_for_end() is False
53
+
54
+ def test_stop_unlinks_ipc_socket_and_clears_state(self):
55
+ p = Player(quality="720")
56
+ sock = _make_socket_file()
57
+ p._ipc_path = sock
58
+ p._process = _FakeProc(alive_polls=1)
59
+ assert os.path.exists(sock)
60
+ p.stop()
61
+ assert not os.path.exists(sock)
62
+ assert p._process is None
63
+ assert p._ipc_path is None
64
+
65
+ def test_stop_is_idempotent_with_no_process(self):
66
+ p = Player(quality="720")
67
+ p.stop() # must not raise
68
+ p.stop()
69
+
70
+ def test_wait_for_end_cleans_socket_on_natural_exit(self):
71
+ p = Player(quality="720")
72
+ sock = _make_socket_file()
73
+ p._ipc_path = sock
74
+ p._process = _FakeProc(alive_polls=0) # already exited
75
+ assert p.wait_for_end(poll_interval=0.01) is True
76
+ assert not os.path.exists(sock)
77
+
78
+ def test_wait_for_end_survives_concurrent_stop(self):
79
+ # The core auto-next race: a newer playback stops this player (nulling
80
+ # self._process) while wait_for_end is mid-poll. Binding the process
81
+ # locally must keep this from NoneType-crashing the poll loop.
82
+ p = Player(quality="720")
83
+ p._ipc_path = _make_socket_file()
84
+ proc = _FakeProc(alive_polls=50)
85
+ p._process = proc
86
+
87
+ def supersede():
88
+ time.sleep(0.02)
89
+ p._process = None # what stop() does to the shared reference
90
+ proc._code = -15 # ...after terminating the underlying process
91
+ proc._remaining = 0
92
+
93
+ t = threading.Thread(target=supersede)
94
+ t.start()
95
+ assert p.wait_for_end(poll_interval=0.01) is True
96
+ t.join()
File without changes
File without changes
File without changes
File without changes