chzzk-python 0.12.0__tar.gz → 0.14.0__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 (117) hide show
  1. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/.env.example +18 -0
  2. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/.github/workflows/docker.yml +6 -3
  3. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/PKG-INFO +27 -1
  4. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/README.md +26 -0
  5. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/README_KO.md +26 -0
  6. chzzk_python-0.14.0/docker-compose.yml +140 -0
  7. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/pyproject.toml +9 -1
  8. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/src/chzzk/_version.py +2 -2
  9. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/src/chzzk/cli/commands/auth.py +1 -1
  10. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/src/chzzk/cli/commands/chat.py +103 -7
  11. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/src/chzzk/cli/formatter.py +4 -3
  12. chzzk_python-0.14.0/src/chzzk/cli/timezone.py +49 -0
  13. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/src/chzzk/cli/writers.py +44 -9
  14. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/src/chzzk/realtime/client.py +15 -13
  15. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/uv.lock +55 -129
  16. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/.dockerignore +0 -0
  17. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/.github/workflows/build.yml +0 -0
  18. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/.github/workflows/ci.yml +0 -0
  19. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/.github/workflows/publish.yml +0 -0
  20. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/.gitignore +0 -0
  21. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/.python-version +0 -0
  22. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/Dockerfile +0 -0
  23. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/LICENSE +0 -0
  24. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/chzzk.spec +0 -0
  25. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/docs/unofficial-chat-websocket-protocol.md +0 -0
  26. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/examples/.env.example +0 -0
  27. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/examples/oauth_server.py +0 -0
  28. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/examples/realtime_chat.py +0 -0
  29. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/examples/realtime_chat_async.py +0 -0
  30. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/examples/session_management.py +0 -0
  31. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/examples/unofficial_chat.py +0 -0
  32. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/examples/unofficial_chat_async.py +0 -0
  33. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/main.py +0 -0
  34. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/scripts/build.py +0 -0
  35. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/src/chzzk/__init__.py +0 -0
  36. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/src/chzzk/api/__init__.py +0 -0
  37. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/src/chzzk/api/base.py +0 -0
  38. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/src/chzzk/api/category.py +0 -0
  39. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/src/chzzk/api/channel.py +0 -0
  40. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/src/chzzk/api/chat.py +0 -0
  41. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/src/chzzk/api/live.py +0 -0
  42. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/src/chzzk/api/restriction.py +0 -0
  43. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/src/chzzk/api/session.py +0 -0
  44. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/src/chzzk/api/user.py +0 -0
  45. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/src/chzzk/auth/__init__.py +0 -0
  46. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/src/chzzk/auth/models.py +0 -0
  47. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/src/chzzk/auth/oauth.py +0 -0
  48. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/src/chzzk/auth/token.py +0 -0
  49. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/src/chzzk/cli/__init__.py +0 -0
  50. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/src/chzzk/cli/commands/__init__.py +0 -0
  51. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/src/chzzk/cli/commands/live.py +0 -0
  52. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/src/chzzk/cli/config.py +0 -0
  53. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/src/chzzk/cli/logging.py +0 -0
  54. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/src/chzzk/cli/main.py +0 -0
  55. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/src/chzzk/client.py +0 -0
  56. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/src/chzzk/constants.py +0 -0
  57. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/src/chzzk/exceptions/__init__.py +0 -0
  58. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/src/chzzk/exceptions/errors.py +0 -0
  59. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/src/chzzk/http/__init__.py +0 -0
  60. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/src/chzzk/http/_base.py +0 -0
  61. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/src/chzzk/http/client.py +0 -0
  62. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/src/chzzk/http/endpoints.py +0 -0
  63. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/src/chzzk/logging.py +0 -0
  64. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/src/chzzk/models/__init__.py +0 -0
  65. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/src/chzzk/models/category.py +0 -0
  66. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/src/chzzk/models/channel.py +0 -0
  67. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/src/chzzk/models/chat.py +0 -0
  68. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/src/chzzk/models/common.py +0 -0
  69. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/src/chzzk/models/live.py +0 -0
  70. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/src/chzzk/models/restriction.py +0 -0
  71. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/src/chzzk/models/session.py +0 -0
  72. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/src/chzzk/models/user.py +0 -0
  73. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/src/chzzk/py.typed +0 -0
  74. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/src/chzzk/realtime/__init__.py +0 -0
  75. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/src/chzzk/unofficial/__init__.py +0 -0
  76. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/src/chzzk/unofficial/api/__init__.py +0 -0
  77. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/src/chzzk/unofficial/api/base.py +0 -0
  78. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/src/chzzk/unofficial/api/chat.py +0 -0
  79. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/src/chzzk/unofficial/api/live.py +0 -0
  80. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/src/chzzk/unofficial/api/user.py +0 -0
  81. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/src/chzzk/unofficial/auth/__init__.py +0 -0
  82. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/src/chzzk/unofficial/auth/cookie.py +0 -0
  83. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/src/chzzk/unofficial/chat/__init__.py +0 -0
  84. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/src/chzzk/unofficial/chat/client.py +0 -0
  85. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/src/chzzk/unofficial/chat/connection.py +0 -0
  86. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/src/chzzk/unofficial/chat/handler.py +0 -0
  87. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/src/chzzk/unofficial/chat/monitor.py +0 -0
  88. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/src/chzzk/unofficial/client.py +0 -0
  89. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/src/chzzk/unofficial/http/__init__.py +0 -0
  90. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/src/chzzk/unofficial/http/_base.py +0 -0
  91. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/src/chzzk/unofficial/http/client.py +0 -0
  92. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/src/chzzk/unofficial/http/endpoints.py +0 -0
  93. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/src/chzzk/unofficial/models/__init__.py +0 -0
  94. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/src/chzzk/unofficial/models/chat.py +0 -0
  95. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/src/chzzk/unofficial/models/live.py +0 -0
  96. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/src/chzzk/unofficial/models/reconnect.py +0 -0
  97. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/src/chzzk/unofficial/models/user.py +0 -0
  98. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/tests/__init__.py +0 -0
  99. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/tests/api/__init__.py +0 -0
  100. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/tests/api/test_category.py +0 -0
  101. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/tests/api/test_channel.py +0 -0
  102. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/tests/api/test_chat.py +0 -0
  103. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/tests/api/test_live.py +0 -0
  104. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/tests/api/test_restriction.py +0 -0
  105. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/tests/api/test_session.py +0 -0
  106. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/tests/api/test_user.py +0 -0
  107. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/tests/auth/__init__.py +0 -0
  108. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/tests/auth/test_oauth.py +0 -0
  109. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/tests/cli/__init__.py +0 -0
  110. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/tests/cli/test_formatter.py +0 -0
  111. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/tests/cli/test_writers.py +0 -0
  112. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/tests/realtime/__init__.py +0 -0
  113. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/tests/realtime/test_client.py +0 -0
  114. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/tests/test_client.py +0 -0
  115. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/tests/unofficial/__init__.py +0 -0
  116. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/tests/unofficial/test_client.py +0 -0
  117. {chzzk_python-0.12.0 → chzzk_python-0.14.0}/tests/unofficial/test_monitor.py +0 -0
@@ -1,6 +1,14 @@
1
1
  # Chzzk CLI Configuration
2
2
  # Copy this file to .env and fill in your values
3
3
 
4
+ # Timezone Configuration
5
+ # Docker 컨테이너 시스템 시간대 (default: Asia/Seoul)
6
+ TZ=Asia/Seoul
7
+
8
+ # 채팅 타임스탬프 및 로그 파일명에 사용되는 시간대 (default: Asia/Seoul)
9
+ # TZ와 별도로 설정 가능하며, 로그 출력에만 영향
10
+ CHZZK_TIMEZONE=Asia/Seoul
11
+
4
12
  # Authentication (Naver Cookie)
5
13
  # 브라우저에서 네이버 로그인 후 개발자 도구에서 쿠키 확인
6
14
  # 1. 브라우저에서 chzzk.naver.com에 로그인
@@ -9,6 +17,16 @@
9
17
  CHZZK_NID_AUT=
10
18
  CHZZK_NID_SES=
11
19
 
20
+ # Channel ID
21
+ # 채팅을 모니터링할 채널 ID
22
+ CHZZK_CHANNEL_ID=
23
+
24
+ # Channel IDs (Docker Compose only)
25
+ # Docker Compose 다중 채널 모니터링 전용 (chat-watch-1, chat-watch-2, chat-watch-3)
26
+ CHZZK_CHANNEL_ID_1=
27
+ CHZZK_CHANNEL_ID_2=
28
+ CHZZK_CHANNEL_ID_3=
29
+
12
30
  # Logging Configuration
13
31
  # Log level: DEBUG, INFO, WARNING, ERROR
14
32
  CHZZK_LOG_LEVEL=WARNING
@@ -145,13 +145,16 @@ jobs:
145
145
  docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
146
146
  $(printf '${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@sha256:%s ' *)
147
147
 
148
- - name: Inspect image
148
+ - name: Get image digest
149
+ id: digest
149
150
  run: |
150
- docker buildx imagetools inspect ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.version }}
151
+ DIGEST=$(docker buildx imagetools inspect ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.version }} --format '{{json .Manifest.Digest}}' | tr -d '"')
152
+ echo "digest=$DIGEST" >> "$GITHUB_OUTPUT"
153
+ echo "Image digest: $DIGEST"
151
154
 
152
155
  - name: Generate artifact attestation
153
156
  uses: actions/attest-build-provenance@v2
154
157
  with:
155
158
  subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
156
- subject-digest: ${{ steps.meta.outputs.version }}
159
+ subject-digest: ${{ steps.digest.outputs.digest }}
157
160
  push-to-registry: true
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: chzzk-python
3
- Version: 0.12.0
3
+ Version: 0.14.0
4
4
  Summary: Unofficial Python SDK for Chzzk (NAVER Live Streaming Platform) API
5
5
  Project-URL: Homepage, https://github.com/hypn4/chzzk-python
6
6
  Project-URL: Repository, https://github.com/hypn4/chzzk-python
@@ -90,6 +90,30 @@ docker run --rm -it \
90
90
 
91
91
  Available tags: `latest`, `X.Y.Z`, `X.Y`, `X`
92
92
 
93
+ ### Docker Compose
94
+
95
+ ```bash
96
+ # Copy and configure environment variables
97
+ cp .env.example .env
98
+ # Edit .env with your credentials and channel ID
99
+
100
+ # Run CLI commands
101
+ docker compose run --rm chzzk --help
102
+ docker compose run --rm chzzk live info CHANNEL_ID
103
+
104
+ # Watch chat (background service with auto-restart)
105
+ docker compose --profile chat up -d chat-watch
106
+
107
+ # Interactive chat mode
108
+ docker compose --profile interactive run --rm chat-interactive
109
+
110
+ # View logs
111
+ docker compose --profile chat logs -f chat-watch
112
+
113
+ # Stop services
114
+ docker compose --profile chat down
115
+ ```
116
+
93
117
  ## Quick Start
94
118
 
95
119
  ```python
@@ -552,6 +576,8 @@ chzzk chat send CHANNEL_ID -i --offline
552
576
  | `CHZZK_CHAT_OUTPUT_FORMAT` | Default chat output format (jsonl, txt) |
553
577
  | `CHZZK_POLL_INTERVAL` | Live status polling interval in seconds (default: 10) |
554
578
  | `CHZZK_AUTO_RECONNECT` | Enable auto-reconnection (default: true, set "false" to disable) |
579
+ | `CHZZK_TIMEZONE` | Timezone for chat timestamps and log filenames (default: Asia/Seoul) |
580
+ | `TZ` | System timezone for Docker containers (default: Asia/Seoul) |
555
581
 
556
582
  ## Examples
557
583
 
@@ -53,6 +53,30 @@ docker run --rm -it \
53
53
 
54
54
  Available tags: `latest`, `X.Y.Z`, `X.Y`, `X`
55
55
 
56
+ ### Docker Compose
57
+
58
+ ```bash
59
+ # Copy and configure environment variables
60
+ cp .env.example .env
61
+ # Edit .env with your credentials and channel ID
62
+
63
+ # Run CLI commands
64
+ docker compose run --rm chzzk --help
65
+ docker compose run --rm chzzk live info CHANNEL_ID
66
+
67
+ # Watch chat (background service with auto-restart)
68
+ docker compose --profile chat up -d chat-watch
69
+
70
+ # Interactive chat mode
71
+ docker compose --profile interactive run --rm chat-interactive
72
+
73
+ # View logs
74
+ docker compose --profile chat logs -f chat-watch
75
+
76
+ # Stop services
77
+ docker compose --profile chat down
78
+ ```
79
+
56
80
  ## Quick Start
57
81
 
58
82
  ```python
@@ -515,6 +539,8 @@ chzzk chat send CHANNEL_ID -i --offline
515
539
  | `CHZZK_CHAT_OUTPUT_FORMAT` | Default chat output format (jsonl, txt) |
516
540
  | `CHZZK_POLL_INTERVAL` | Live status polling interval in seconds (default: 10) |
517
541
  | `CHZZK_AUTO_RECONNECT` | Enable auto-reconnection (default: true, set "false" to disable) |
542
+ | `CHZZK_TIMEZONE` | Timezone for chat timestamps and log filenames (default: Asia/Seoul) |
543
+ | `TZ` | System timezone for Docker containers (default: Asia/Seoul) |
518
544
 
519
545
  ## Examples
520
546
 
@@ -53,6 +53,30 @@ docker run --rm -it \
53
53
 
54
54
  사용 가능한 태그: `latest`, `X.Y.Z`, `X.Y`, `X`
55
55
 
56
+ ### Docker Compose
57
+
58
+ ```bash
59
+ # 환경 변수 파일 복사 및 설정
60
+ cp .env.example .env
61
+ # .env 파일에 인증 정보와 채널 ID 입력
62
+
63
+ # CLI 명령어 실행
64
+ docker compose run --rm chzzk --help
65
+ docker compose run --rm chzzk live info CHANNEL_ID
66
+
67
+ # 채팅 모니터링 (백그라운드 서비스, 자동 재시작)
68
+ docker compose --profile chat up -d chat-watch
69
+
70
+ # 대화형 채팅 모드
71
+ docker compose --profile interactive run --rm chat-interactive
72
+
73
+ # 로그 확인
74
+ docker compose --profile chat logs -f chat-watch
75
+
76
+ # 서비스 종료
77
+ docker compose --profile chat down
78
+ ```
79
+
56
80
  ## 빠른 시작
57
81
 
58
82
  ```python
@@ -515,6 +539,8 @@ chzzk chat send CHANNEL_ID -i --offline
515
539
  | `CHZZK_CHAT_OUTPUT_FORMAT` | 기본 채팅 출력 형식 (jsonl, txt) |
516
540
  | `CHZZK_POLL_INTERVAL` | 라이브 상태 폴링 간격 (초, 기본값: 10) |
517
541
  | `CHZZK_AUTO_RECONNECT` | 자동 재연결 활성화 (기본값: true, 비활성화: "false") |
542
+ | `CHZZK_TIMEZONE` | 채팅 타임스탬프 및 로그 파일명 시간대 (기본값: Asia/Seoul) |
543
+ | `TZ` | Docker 컨테이너 시스템 시간대 (기본값: Asia/Seoul) |
518
544
 
519
545
  ## 예제 코드
520
546
 
@@ -0,0 +1,140 @@
1
+ # =============================================================================
2
+ # chzzk-python Docker Compose
3
+ # =============================================================================
4
+ #
5
+ # Setup:
6
+ # cp .env.example .env
7
+ # # Edit .env with your CHZZK_CHANNEL_ID_1, CHZZK_CHANNEL_ID_2, ...
8
+ #
9
+ # Usage:
10
+ # # QR 로그인으로 쿠키 저장 (최초 1회)
11
+ # mkdir -p .chzzk .logs && chmod 777 .chzzk .logs
12
+ # docker compose run --rm -v ./.chzzk:/home/chzzk/.chzzk chzzk auth qr
13
+ # # 또는 docker run 사용:
14
+ # docker run --rm -it -v ./.chzzk:/home/chzzk/.chzzk ghcr.io/hypn4/chzzk-python:latest auth qr
15
+ # # 또는 호스트 사용자 권한으로 실행:
16
+ # docker run --rm -it --user $(id -u):$(id -g) -v ./.chzzk:/home/chzzk/.chzzk ghcr.io/hypn4/chzzk-python:latest auth qr
17
+ #
18
+ # # CLI 명령어 실행
19
+ # docker compose run --rm chzzk --help
20
+ # docker compose run --rm chzzk live info CHANNEL_ID
21
+ #
22
+ # # 채팅 모니터링 (백그라운드)
23
+ # # .env에 CHZZK_CHANNEL_ID_1, CHZZK_CHANNEL_ID_2, ... 설정 후:
24
+ # docker compose --profile chat up -d
25
+ # docker compose --profile chat logs -f
26
+ # docker compose --profile chat down
27
+ # # 특정 채널만 실행:
28
+ # docker compose --profile chat up -d chat-watch-1
29
+ #
30
+ # # 대화형 채팅
31
+ # docker compose --profile interactive run --rm chat-interactive
32
+ #
33
+ # =============================================================================
34
+
35
+ # YAML Anchor: 채팅 모니터링 서비스 공통 설정
36
+ x-chat-watch-base: &chat-watch-base
37
+ image: ghcr.io/hypn4/chzzk-python:latest
38
+ restart: unless-stopped
39
+ profiles:
40
+ - chat
41
+ environment:
42
+ - TZ=${TZ:-Asia/Seoul}
43
+ - CHZZK_TIMEZONE=${CHZZK_TIMEZONE:-Asia/Seoul}
44
+ - CHZZK_NID_AUT=${CHZZK_NID_AUT:-}
45
+ - CHZZK_NID_SES=${CHZZK_NID_SES:-}
46
+ - CHZZK_LOG_LEVEL=${CHZZK_LOG_LEVEL:-INFO}
47
+ - CHZZK_POLL_INTERVAL=${CHZZK_POLL_INTERVAL:-10}
48
+ volumes:
49
+ - ./.logs:/home/chzzk/logs
50
+ - ./.chzzk:/home/chzzk/.chzzk
51
+
52
+ services:
53
+ chzzk:
54
+ image: ghcr.io/hypn4/chzzk-python:latest
55
+ container_name: chzzk
56
+ restart: "no"
57
+ stdin_open: true
58
+ tty: true
59
+ environment:
60
+ - TZ=${TZ:-Asia/Seoul}
61
+ - CHZZK_TIMEZONE=${CHZZK_TIMEZONE:-Asia/Seoul}
62
+ - CHZZK_NID_AUT=${CHZZK_NID_AUT:-}
63
+ - CHZZK_NID_SES=${CHZZK_NID_SES:-}
64
+ - CHZZK_LOG_LEVEL=${CHZZK_LOG_LEVEL:-INFO}
65
+ - CHZZK_POLL_INTERVAL=${CHZZK_POLL_INTERVAL:-10}
66
+ - CHZZK_AUTO_RECONNECT=${CHZZK_AUTO_RECONNECT:-true}
67
+ volumes:
68
+ - ./.logs:/home/chzzk/logs
69
+ # Uncomment to persist cookies (run: docker compose run --rm chzzk auth qr)
70
+ # - ./.chzzk:/home/chzzk/.chzzk
71
+ # Override command as needed:
72
+ # command: ["chat", "watch", "CHANNEL_ID"]
73
+ # command: ["chat", "watch", "CHANNEL_ID", "--output-dir", "/home/chzzk/logs"]
74
+ # command: ["live", "info", "CHANNEL_ID"]
75
+
76
+ # Multi-channel chat monitoring (uses x-chat-watch-base anchor)
77
+ # 각 서비스는 CHZZK_CHANNEL_ID_N 환경변수를 사용하여 채널 지정
78
+ chat-watch-1:
79
+ <<: *chat-watch-base
80
+ container_name: chzzk-chat-watch-1
81
+ command:
82
+ - chat
83
+ - watch
84
+ - ${CHZZK_CHANNEL_ID_1:-}
85
+ - --output-dir
86
+ - /home/chzzk/logs
87
+ - --offline
88
+ - --auto-reconnect
89
+
90
+ chat-watch-2:
91
+ <<: *chat-watch-base
92
+ container_name: chzzk-chat-watch-2
93
+ command:
94
+ - chat
95
+ - watch
96
+ - ${CHZZK_CHANNEL_ID_2:-}
97
+ - --output-dir
98
+ - /home/chzzk/logs
99
+ - --offline
100
+ - --auto-reconnect
101
+
102
+ chat-watch-3:
103
+ <<: *chat-watch-base
104
+ container_name: chzzk-chat-watch-3
105
+ command:
106
+ - chat
107
+ - watch
108
+ - ${CHZZK_CHANNEL_ID_3:-}
109
+ - --output-dir
110
+ - /home/chzzk/logs
111
+ - --offline
112
+ - --auto-reconnect
113
+
114
+ # Example: Interactive chat mode
115
+ chat-interactive:
116
+ image: ghcr.io/hypn4/chzzk-python:latest
117
+ container_name: chzzk-chat-interactive
118
+ restart: "no"
119
+ profiles:
120
+ - interactive
121
+ stdin_open: true
122
+ tty: true
123
+ environment:
124
+ - TZ=${TZ:-Asia/Seoul}
125
+ - CHZZK_TIMEZONE=${CHZZK_TIMEZONE:-Asia/Seoul}
126
+ - CHZZK_NID_AUT=${CHZZK_NID_AUT:-}
127
+ - CHZZK_NID_SES=${CHZZK_NID_SES:-}
128
+ - CHZZK_LOG_LEVEL=${CHZZK_LOG_LEVEL:-INFO}
129
+ volumes:
130
+ - ./.logs:/home/chzzk/logs
131
+ # - ./.chzzk:/home/chzzk/.chzzk
132
+ command:
133
+ - chat
134
+ - send
135
+ - ${CHZZK_CHANNEL_ID:-}
136
+ - --interactive
137
+ - --output-dir
138
+ - /home/chzzk/logs
139
+ - --offline
140
+ - --auto-reconnect
@@ -54,14 +54,18 @@ Changelog = "https://github.com/hypn4/chzzk-python/blob/main/CHANGELOG.md"
54
54
  [dependency-groups]
55
55
  dev = [
56
56
  "flask>=3.1.2",
57
- "mypy>=1.19.1",
57
+ "prompt-toolkit>=3.0.52",
58
58
  "pyinstaller>=6.18.0",
59
+ "pyright>=1.1.408",
59
60
  "pytest>=9.0.2",
60
61
  "pytest-asyncio>=1.3.0",
61
62
  "pytest-httpx>=0.35.0",
62
63
  "python-dotenv>=1.2.1",
63
64
  "pyzbar>=0.1.9",
65
+ "qrcode>=8.2",
66
+ "rich>=14.3.1",
64
67
  "ruff>=0.14.13",
68
+ "typer>=0.15.0",
65
69
  ]
66
70
 
67
71
  [build-system]
@@ -94,6 +98,10 @@ asyncio_mode = "auto"
94
98
  asyncio_default_fixture_loop_scope = "function"
95
99
  testpaths = ["tests"]
96
100
 
101
+ [tool.pyright]
102
+ pythonVersion = "3.12"
103
+ typeCheckingMode = "standard"
104
+
97
105
  [tool.ruff]
98
106
  line-length = 100
99
107
  target-version = "py312"
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
28
28
  commit_id: COMMIT_ID
29
29
  __commit_id__: COMMIT_ID
30
30
 
31
- __version__ = version = '0.12.0'
32
- __version_tuple__ = version_tuple = (0, 12, 0)
31
+ __version__ = version = '0.14.0'
32
+ __version_tuple__ = version_tuple = (0, 14, 0)
33
33
 
34
34
  __commit_id__ = commit_id = None
@@ -297,7 +297,7 @@ def _generate_qr_ascii(session: str) -> str:
297
297
 
298
298
  qr = qrcode.QRCode(
299
299
  version=1,
300
- error_correction=qrcode.constants.ERROR_CORRECT_L,
300
+ error_correction=qrcode.constants.ERROR_CORRECT_L, # type: ignore[reportAttributeAccessIssue]
301
301
  box_size=1,
302
302
  border=1,
303
303
  )
@@ -7,7 +7,6 @@ import contextlib
7
7
  import json
8
8
  import logging
9
9
  import signal
10
- from datetime import datetime
11
10
  from pathlib import Path
12
11
  from typing import Annotated
13
12
 
@@ -16,8 +15,15 @@ from prompt_toolkit import PromptSession
16
15
  from prompt_toolkit.patch_stdout import patch_stdout
17
16
  from rich.console import Console
18
17
 
18
+ from chzzk.cli import timezone as tz
19
19
  from chzzk.cli.formatter import ChatFormatter, FormatConfig
20
- from chzzk.cli.writers import ChatWriter, OutputFormat, create_writer, generate_chat_log_filename
20
+ from chzzk.cli.writers import (
21
+ ChatWriter,
22
+ OutputFormat,
23
+ create_writer,
24
+ generate_chat_log_filename,
25
+ rotate_writer,
26
+ )
21
27
  from chzzk.constants import StatusText
22
28
  from chzzk.exceptions import ChatConnectionError, ChatNotLiveError
23
29
  from chzzk.unofficial import (
@@ -57,7 +63,7 @@ def format_chat_message_json(msg: ChatMessage) -> str:
57
63
  return json.dumps(
58
64
  {
59
65
  "type": "chat",
60
- "timestamp": datetime.now().isoformat(),
66
+ "timestamp": tz.now().isoformat(),
61
67
  "user_id_hash": msg.user_id_hash,
62
68
  "nickname": msg.nickname,
63
69
  "content": msg.content,
@@ -71,7 +77,7 @@ def format_donation_message_json(msg: DonationMessage) -> str:
71
77
  return json.dumps(
72
78
  {
73
79
  "type": "donation",
74
- "timestamp": datetime.now().isoformat(),
80
+ "timestamp": tz.now().isoformat(),
75
81
  "user_id_hash": msg.user_id_hash,
76
82
  "nickname": msg.nickname,
77
83
  "content": msg.content,
@@ -85,7 +91,7 @@ def format_sent_message_json(content: str) -> str:
85
91
  return json.dumps(
86
92
  {
87
93
  "type": "sent",
88
- "timestamp": datetime.now().isoformat(),
94
+ "timestamp": tz.now().isoformat(),
89
95
  "content": content,
90
96
  }
91
97
  )
@@ -216,6 +222,7 @@ def _run_watch_console(
216
222
  formatter = ChatFormatter(format_config)
217
223
  # Track writer created from output_dir (needs to be closed on exit)
218
224
  output_dir_writer: ChatWriter | None = None
225
+ current_live_id: int | None = None
219
226
 
220
227
  async def run_chat() -> None:
221
228
  async with AsyncUnofficialChzzkClient(nid_aut=nid_aut, nid_ses=nid_ses) as client:
@@ -244,6 +251,49 @@ def _run_watch_console(
244
251
 
245
252
  @chat.on_live
246
253
  async def handle_live(event: StatusChangeEvent) -> None:
254
+ nonlocal writer, output_dir_writer, current_live_id
255
+
256
+ # Rotate log file when live_id changes in output_dir mode
257
+ if output_dir and event.live_id != current_live_id:
258
+ try:
259
+ open_date: str | None = None
260
+ try:
261
+ new_live_detail = await client.live.get_live_detail(channel_id)
262
+ open_date = new_live_detail.open_date
263
+ except Exception:
264
+ logger.warning(
265
+ "Failed to get live detail for log rotation, using local date"
266
+ )
267
+
268
+ new_writer, new_path = rotate_writer(
269
+ old_writer=output_dir_writer,
270
+ output_dir=output_dir,
271
+ channel_id=channel_id,
272
+ new_live_id=event.live_id,
273
+ open_date=open_date,
274
+ output_format=output_format,
275
+ )
276
+ output_dir_writer = new_writer
277
+ writer = new_writer
278
+ current_live_id = event.live_id
279
+
280
+ if json_output:
281
+ console.print(
282
+ json.dumps(
283
+ {
284
+ "event": "log_rotated",
285
+ "new_file": str(new_path),
286
+ "live_id": event.live_id,
287
+ }
288
+ )
289
+ )
290
+ else:
291
+ console.print(f"[dim]Log rotated: {new_path}[/dim]")
292
+ except OSError as e:
293
+ logger.warning(f"Failed to rotate log file: {e}")
294
+ if not json_output:
295
+ console.print(f"[yellow]Warning:[/yellow] Log rotation failed: {e}")
296
+
247
297
  if json_output:
248
298
  console.print(
249
299
  json.dumps(
@@ -316,7 +366,7 @@ def _run_watch_console(
316
366
  raise typer.Exit(1) from None
317
367
 
318
368
  # Create writer from output_dir if specified (after getting live_detail)
319
- nonlocal writer, output_dir_writer
369
+ nonlocal writer, output_dir_writer, current_live_id
320
370
  if output_dir and not writer:
321
371
  try:
322
372
  output_dir.mkdir(parents=True, exist_ok=True)
@@ -329,6 +379,7 @@ def _run_watch_console(
329
379
  )
330
380
  output_dir_writer = create_writer(output_path, output_format)
331
381
  writer = output_dir_writer
382
+ current_live_id = live_detail.live_id
332
383
  if not json_output:
333
384
  console.print(f"[dim]Saving chat to: {output_path}[/dim]")
334
385
  except OSError as e:
@@ -652,6 +703,7 @@ def _run_interactive_chat_console(
652
703
  formatter = ChatFormatter(format_config)
653
704
  # Track writer created from output_dir (needs to be closed on exit)
654
705
  output_dir_writer: ChatWriter | None = None
706
+ current_live_id: int | None = None
655
707
 
656
708
  async def run_chat() -> None:
657
709
  async with AsyncUnofficialChzzkClient(nid_aut=nid_aut, nid_ses=nid_ses) as client:
@@ -680,6 +732,49 @@ def _run_interactive_chat_console(
680
732
 
681
733
  @chat.on_live
682
734
  async def handle_live(event: StatusChangeEvent) -> None:
735
+ nonlocal writer, output_dir_writer, current_live_id
736
+
737
+ # Rotate log file when live_id changes in output_dir mode
738
+ if output_dir and event.live_id != current_live_id:
739
+ try:
740
+ open_date: str | None = None
741
+ try:
742
+ new_live_detail = await client.live.get_live_detail(channel_id)
743
+ open_date = new_live_detail.open_date
744
+ except Exception:
745
+ logger.warning(
746
+ "Failed to get live detail for log rotation, using local date"
747
+ )
748
+
749
+ new_writer, new_path = rotate_writer(
750
+ old_writer=output_dir_writer,
751
+ output_dir=output_dir,
752
+ channel_id=channel_id,
753
+ new_live_id=event.live_id,
754
+ open_date=open_date,
755
+ output_format=output_format,
756
+ )
757
+ output_dir_writer = new_writer
758
+ writer = new_writer
759
+ current_live_id = event.live_id
760
+
761
+ if json_output:
762
+ console.print(
763
+ json.dumps(
764
+ {
765
+ "event": "log_rotated",
766
+ "new_file": str(new_path),
767
+ "live_id": event.live_id,
768
+ }
769
+ )
770
+ )
771
+ else:
772
+ console.print(f"[dim]Log rotated: {new_path}[/dim]")
773
+ except OSError as e:
774
+ logger.warning(f"Failed to rotate log file: {e}")
775
+ if not json_output:
776
+ console.print(f"[yellow]Warning:[/yellow] Log rotation failed: {e}")
777
+
683
778
  if json_output:
684
779
  console.print(
685
780
  json.dumps(
@@ -752,7 +847,7 @@ def _run_interactive_chat_console(
752
847
  raise typer.Exit(1) from None
753
848
 
754
849
  # Create writer from output_dir if specified (after getting live_detail)
755
- nonlocal writer, output_dir_writer
850
+ nonlocal writer, output_dir_writer, current_live_id
756
851
  if output_dir and not writer:
757
852
  try:
758
853
  output_dir.mkdir(parents=True, exist_ok=True)
@@ -765,6 +860,7 @@ def _run_interactive_chat_console(
765
860
  )
766
861
  output_dir_writer = create_writer(output_path, output_format)
767
862
  writer = output_dir_writer
863
+ current_live_id = live_detail.live_id
768
864
  if not json_output:
769
865
  console.print(f"[dim]Saving chat to: {output_path}[/dim]")
770
866
  except OSError as e:
@@ -3,11 +3,12 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  from dataclasses import dataclass
6
- from datetime import datetime
7
6
  from typing import TYPE_CHECKING, Any
8
7
 
9
8
  from rich.markup import escape
10
9
 
10
+ from chzzk.cli import timezone as tz
11
+
11
12
  if TYPE_CHECKING:
12
13
  from chzzk.unofficial import ChatMessage, ChatProfile, DonationMessage
13
14
 
@@ -119,7 +120,7 @@ class ChatFormatter:
119
120
 
120
121
  def _get_timestamp(self) -> str:
121
122
  """Get formatted timestamp string."""
122
- return datetime.now().strftime(self.config.time_format)
123
+ return tz.now().strftime(self.config.time_format)
123
124
 
124
125
  def _escape_markup(self, text: str) -> str:
125
126
  """Escape Rich markup characters in text.
@@ -214,7 +215,7 @@ class ChatFormatter:
214
215
  """
215
216
  if not message_time:
216
217
  return ""
217
- dt = datetime.fromtimestamp(message_time / 1000)
218
+ dt = tz.from_timestamp(message_time / 1000)
218
219
  return dt.strftime(self.config.time_format)
219
220
 
220
221
  def _get_pay_type_str(self, pay_type: str | None) -> str:
@@ -0,0 +1,49 @@
1
+ """Timezone utilities for CLI output.
2
+
3
+ This module provides centralized timezone handling for consistent timestamp
4
+ display across different environments (local, Docker, etc.).
5
+
6
+ The timezone can be configured via the CHZZK_TIMEZONE environment variable.
7
+ Default timezone is Asia/Seoul (KST).
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import os
13
+ from datetime import datetime
14
+ from zoneinfo import ZoneInfo
15
+
16
+ DEFAULT_TIMEZONE = "Asia/Seoul"
17
+
18
+
19
+ def get_timezone() -> ZoneInfo:
20
+ """Get the configured timezone.
21
+
22
+ Returns:
23
+ ZoneInfo object for the configured timezone.
24
+ Uses CHZZK_TIMEZONE environment variable if set,
25
+ otherwise defaults to Asia/Seoul.
26
+ """
27
+ tz_name = os.environ.get("CHZZK_TIMEZONE", DEFAULT_TIMEZONE)
28
+ return ZoneInfo(tz_name)
29
+
30
+
31
+ def now() -> datetime:
32
+ """Get current datetime in the configured timezone.
33
+
34
+ Returns:
35
+ Current datetime with timezone info.
36
+ """
37
+ return datetime.now(get_timezone())
38
+
39
+
40
+ def from_timestamp(timestamp: float) -> datetime:
41
+ """Convert Unix timestamp to datetime in the configured timezone.
42
+
43
+ Args:
44
+ timestamp: Unix timestamp (seconds since epoch).
45
+
46
+ Returns:
47
+ Datetime object in the configured timezone.
48
+ """
49
+ return datetime.fromtimestamp(timestamp, get_timezone())