Skip to content

Commit b5e0661

Browse files
authored
Merge pull request #8 from sakkyoi/rewrite-architecture
Rewrite architecture
2 parents 7ddb403 + 11ad6b1 commit b5e0661

File tree

10 files changed

+481
-211
lines changed

10 files changed

+481
-211
lines changed

.github/workflows/main.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ jobs:
2525
- uses: actions/checkout@v4
2626
- uses: actions/setup-python@v4
2727
with:
28-
python-version: 3.8
28+
python-version: 3.12
2929
- run: pip install -r requirements.txt pyinstaller Pillow
3030
- run: pyinstaller main.py --onefile --collect-data=grapheme --name=ncp-${{ matrix.name }}
3131
- run: ./dist/ncp-${{ matrix.name }} --help

README.md

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
ncp-downloader is a tool to do network testing by download videos from a well-known video platform.
1+
`ncp-downloader` is a tool for network performance testing by downloading videos from a well-known online video platform.
22

33
# Installation
4-
download the latest release from the [releases page](https://github.com/sakkyoi/ncp-downloader/releases/latest) and extract it.
4+
Download the latest release from the [Releases](https://github.com/sakkyoi/ncp-downloader/releases/latest) page
5+
and extract it to the directory of your choice on your local machine.
56

67
# Usage
78
`ncp QUERY [OUTPUT_DIR] [OPTIONS]`
@@ -11,28 +12,30 @@ QUERY is a URL of video or channel.
1112
```
1213
-r RESOLUTION, --resolution RESOLUTION Target resolution. Defaults to highest resolution.
1314
-R, --resume Resume download.
14-
-e, --experimental Experimental download method.
1515
-t, --transcode Transcode video.
1616
--ffmpeg FFMPEG Path to ffmpeg. Defaults to ffmpeg in PATH.
1717
--vcodec VCODEC Video codec for transcoding.
1818
--acodec ACODEC Audio codec for transcoding.
1919
--ffmpeg-options FFMPEG_OPTIONS Additional ffmpeg options. (e.g. --ffmpeg-options "-acodec copy -vcodec copy")
2020
--thread THREAD Number of threads for downloading. Defaults to 1. (highly not recommended)
21+
--select-manually Select video manually. This option only works with channel.
2122
--username USERNAME Username for login.
2223
--password PASSWORD Password for login.
2324
--debug Enable debug mode.
2425
--help Show help message.
2526
```
26-
**If username and password are provided, a token will be generated and saved in the current directory.
27+
28+
**If login credentials are provided, a session token will be generated and saved locally.
2729
DO NOT SHARE THE TOKEN WITH ANYONE.**<br>
28-
Sometime this tool may not work properly, you can delete temp files and folder to make it re-download the video.
29-
(feel free to modify the json file when you know what you are doing)
30+
Sometimes this tool may not function properly, delete temp files and folder to make it re-download the video.<br>
31+
(Feel free to modify the .json file if you understand what you are doing.)
3032

31-
## `This tool may cause account suspension or ban. Use it at your own risk.`
33+
## `Using this tool may lead to account suspension or ban. Use it at your own discretion.`
3234

3335
# Disclaimer
3436
Please do use this tool responsibly and respect the rights of the content creators.<br>
35-
Every data downloaded using this tool should be for personal use only and should be just for network testing purposes.
37+
You should **ONLY** use this tool for network performance testing under any circumstances.<br>
38+
Data downloaded using this tool should be for personal use only. Obtain permission from the original creator before use.
3639

3740
# License
3841
This project is licensed under the LGPLv3 License - see the [LICENSE](LICENSE) file for details.

api/api.py

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,6 @@ def __init__(self, site_base: str, username: Optional[str], password: Optional[s
7777
self.api_public_status = f'{self.api_base}/video_pages/%s/public_status' # content_code
7878
self.api_session_id = f'{self.api_base}/video_pages/%s/session_ids' # content_code
7979
self.api_video_list = f'{self.api_base}/fanclub_sites/%s/video_pages?vod_type=%d&page=%d&per_page=%d&sort=%s'
80-
self.api_views_comments = f'{self.api_base}/fanclub_sites/%s/views_comments' # channel_id
8180
self.api_video_index = 'https://hls-auth.cloud.stream.co.jp/auth/index.m3u8?session_id=%s' # session_id
8281

8382
def __initial_api(self) -> Tuple[str, str, str]:
@@ -120,14 +119,6 @@ def list_channels(self) -> list:
120119
r = requests.get(self.api_channels, headers=self.headers)
121120
return r.json()['data']['content_providers']
122121

123-
def list_views_comments(self, channel_id: ChannelID) -> list:
124-
"""Get count of views and comments of channel from channel id \n
125-
This api using different method to get video list \n
126-
This api can set specific video, like ?content_codes[]=xxxxxx&content_codes[]=xxxxxx&..., \
127-
but it's not implemented here"""
128-
r = requests.get(self.api_views_comments % channel_id, headers=self.headers)
129-
return r.json()['data']['video_aggregate_infos']
130-
131122
def list_videos(self,
132123
channel_id: ChannelID,
133124
vod_type: int = 0,
@@ -145,11 +136,6 @@ def list_videos(self,
145136
video_list += r.json()['data']['video_pages']['list']
146137
return video_list
147138

148-
def list_videos_x(self, channel_id: ChannelID) -> list:
149-
"""Get video list of channel from channel id by views and comments count list"""
150-
views_comments = self.list_views_comments(channel_id)
151-
return [ContentCode(video['content_code']) for video in views_comments]
152-
153139
def list_lives(self, channel_id: str, live_type) -> list:
154140
"""Get live list of channel from channel id"""
155141
# TODO: I'm lazy, and who cares about it?

main.py

Lines changed: 64 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,22 @@
11
import sys
2+
import platform
23

34
import click
4-
from click import confirm
5+
import inquirer
56
import typer
67
from typing_extensions import Annotated
78
from typing import Optional
8-
from rich.console import Console
99
from urllib.parse import urlparse
1010
from pathlib import Path
11+
import pylibimport
1112

1213
from api.api import NCP, ContentCode
1314
from util.ffmpeg import FFMPEG
1415
from util.m3u8_downloader import M3U8Downloader
1516
from util.channel_downloader import ChannelDownloader
17+
from util.progress import ProgressManager
18+
19+
__import__('util.inquirer_console_render') # hook for inquirer console render
1620

1721

1822
class Resolution(click.ParamType):
@@ -71,14 +75,6 @@ def main(
7175
help='Resume download.',
7276
),
7377
] = None,
74-
experimental: Annotated[
75-
Optional[bool],
76-
typer.Option(
77-
'--experimental/--normal', '-e/-ne',
78-
show_default=False,
79-
help='Experimental download method',
80-
),
81-
] = None,
8278
yes: Annotated[
8379
Optional[bool],
8480
typer.Option(
@@ -130,9 +126,17 @@ def main(
130126
typer.Option(
131127
'--thread',
132128
show_default=True,
133-
help='Number of threads.',
129+
help='Number of threads. Be careful with this option.',
134130
),
135131
] = 1,
132+
select_manually: Annotated[
133+
bool,
134+
typer.Option(
135+
'--select-manually',
136+
show_default=False,
137+
help='Select which video to download manually. This option only works with channel.',
138+
),
139+
] = False,
136140
username: Annotated[
137141
str,
138142
typer.Option(
@@ -157,10 +161,8 @@ def main(
157161
] = False,
158162
) -> None:
159163
"""The NCP Downloader"""
160-
err_console = Console(stderr=True)
161-
162-
# nico
163-
nico = NCP(urlparse(query).netloc, username, password)
164+
api_client = NCP(urlparse(query).netloc, username, password)
165+
progress_manager = ProgressManager()
164166

165167
try:
166168
# Check ffmpeg if transcode is enabled
@@ -170,60 +172,83 @@ def main(
170172

171173
# If yes is enabled, skip all confirmation
172174
if yes:
173-
experimental = resume = yes
175+
resume = yes
174176

175177
# tell user multithreading is dengerous
176-
if (thread > 1 and
177-
not confirm('Download with multithreading may got you banned from Server. Continue?', default=False)):
178-
raise RuntimeError('Aborted.')
178+
# can not be skipped by --yes
179+
if thread > 1:
180+
with progress_manager.pause():
181+
if inquirer.prompt([inquirer.List('thread',
182+
message='Multithreading is dangerous, are you sure to continue?',
183+
choices=['Yes', 'No'], default='No')],
184+
raise_keyboard_interrupt=True)['thread'] == 'No':
185+
raise RuntimeError('Aborted.')
179186

180187
# Check if query is channel or video
181-
if nico.get_channel_id(query) is None:
188+
if api_client.get_channel_id(query) is None:
182189
query = urlparse(query).path.strip('/').split('/')[-1]
183-
session_id = nico.get_session_id(ContentCode(query))
190+
session_id = api_client.get_session_id(ContentCode(query))
184191

185192
# Check if video exists
186193
if session_id is None:
187194
raise ValueError('Video not found or permission denied.')
188195

189-
output_name, _ = nico.get_video_name(ContentCode(query))
196+
output_name, _ = api_client.get_video_name(ContentCode(query))
190197

191198
output = str(Path(output).joinpath(output_name))
192199

193-
M3U8Downloader(nico, session_id, output, resolution, resume, transcode,
194-
ffmpeg, vcodec, acodec, ffmpeg_options, thread)
200+
with progress_manager:
201+
m3u8_downloader = M3U8Downloader(api_client, progress_manager, session_id, output, resolution, resume,
202+
transcode, ffmpeg, vcodec, acodec, ffmpeg_options, thread)
203+
if not m3u8_downloader.start():
204+
raise RuntimeError('Failed to download video.')
195205
else:
206+
# warning for downloading whole channel if --yes is not set
196207
if not yes:
197-
if not confirm('Sure to download whole channel?', default=True):
198-
raise RuntimeError('Aborted.')
199-
200-
if experimental is None:
201-
experimental = confirm('Using experimental download method?', default=True)
208+
with progress_manager.pause():
209+
if inquirer.prompt([
210+
inquirer.List('channel', message='Sure to download whole channel?',
211+
choices=['Sure', 'No'], default='No')
212+
], raise_keyboard_interrupt=True)['channel'] == 'No':
213+
raise RuntimeError('Aborted.')
202214

203215
# Get channel infomation
204-
channel_id = nico.get_channel_id(query)
205-
channel_name = nico.get_channel_info(channel_id)['fanclub_site_name']
216+
channel_id = api_client.get_channel_id(query)
217+
channel_name = api_client.get_channel_info(channel_id)['fanclub_site_name']
206218

207219
# Get video list
208-
if experimental:
209-
video_list = nico.list_videos_x(channel_id)
210-
else:
211-
video_list = nico.list_videos(channel_id)
212-
video_list = [ContentCode(video['content_code']) for video in video_list]
220+
video_list = api_client.list_videos(channel_id)
221+
video_list = [ContentCode(video['content_code']) for video in video_list]
213222

214223
output = str(Path(output).joinpath(channel_name))
215224

216-
ChannelDownloader(nico, channel_id, video_list, output, resolution, resume,
217-
transcode, ffmpeg, vcodec, acodec, ffmpeg_options, thread)
225+
with progress_manager:
226+
channel_downloader = ChannelDownloader(api_client, progress_manager, channel_id, video_list, output,
227+
resolution, resume,
228+
transcode, ffmpeg, vcodec, acodec, ffmpeg_options,
229+
thread, select_manually)
230+
channel_downloader.start()
218231
except Exception as e:
219232
# Raise exception again if debug is enabled
220233
if debug:
221234
raise e
222235
# Print error message and exit if debug is disabled
223236
else:
224-
err_console.print(f'[red]{e}[/red]')
237+
progress_manager.live.console.print(f'{e}', style='red')
225238
sys.exit(1)
226239

227240

228241
if __name__ == "__main__":
242+
# find all the .pyd(win) or .so(linux and macos), files in the current directory
243+
if platform.system() == 'Windows':
244+
pyds = Path('.').glob('*.pyd')
245+
elif platform.system() == 'Linux' or platform.system() == 'Darwin':
246+
pyds = Path('.').glob('*.so')
247+
else:
248+
raise RuntimeError('Unsupported platform')
249+
250+
# import all the .pyd or .so files
251+
for pyd in pyds:
252+
pylibimport.import_module(pyd.stem)
253+
229254
typer.run(main)

requirements.txt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@ click==8.1.7
22
requests==2.32.3
33
m3u8==5.1.0
44
pycryptodome==3.19.1
5-
alive-progress==3.1.5
5+
inquirer==3.3.0
66
tinydb==4.8.0
77
pathvalidate==3.2.0
88
typer[all]==0.12.3
99
rich==13.7.1
10-
pyjwt==2.8.0
10+
pyjwt==2.8.0
11+
setuptools==72.1.0
12+
pylibimport==1.9.2

0 commit comments

Comments
 (0)