From e27b9ae8165bf6499dc6cd90fb38248d791d77f8 Mon Sep 17 00:00:00 2001 From: Hayao-H <55943986+Hayao-H@users.noreply.github.com> Date: Thu, 12 Sep 2024 18:24:56 +0900 Subject: [PATCH] feature/dommand (#648) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [追加]DMSのストリームに対応 * [追加]新サーバーに対応 * [追加]ストリーム長を記録 * [追加]初期化ファイルのDL機能を追加 * [追加]再生機能を追加 * [変更]視聴APIのffmpegとの互換性の問題を修正 * [変更]DMS動画をアプリで開く機能を追加 * [変更]denoに移行 * [追加]視聴用のAPIを追加 * [追加]視聴ページ・NG機能のバックグラウンドを実装 * [変更]NGAPIの不具合を修正 * [追加]NG機能を追加 * [変更]動画再生機能の不具合を修正 * [追加]下部のパネルをBlazorに移行 * [追加]DL後のファイル処理機能を追加 * [変更]不要なファイルを削除 * [変更]セッション管理を集約 * [変更]ログアウト時に保存したCookieを削除するように変更 --- Niconicome/App.xaml.cs | 1 - Niconicome/Design/Auth/cookie.pu | 56 + Niconicome/Extensions/System/System.cs | 9 + Niconicome/Main.razor | 1 + Niconicome/Models/Auth/AutoLogin.cs | 34 +- .../Auth/Cookie/NiconicoCookieManager.cs | 106 + .../Auth/Error/ChromeSharedLoginError.cs | 15 + Niconicome/Models/Auth/FirefoxSharedLogin.cs | 14 +- Niconicome/Models/Auth/Session.cs | 68 - Niconicome/Models/Auth/SharedLoginBase.cs | 20 +- .../Models/Auth/StoreFirefoxSharedLogin.cs | 2 +- Niconicome/Models/Auth/StoredCookieLogin.cs | 63 + Niconicome/Models/Auth/Webview2SharedLogin.cs | 38 +- Niconicome/Models/Const/LocalConstant.cs | 2 + Niconicome/Models/Const/NetConstant.cs | 34 +- .../Core/V2/Update/AddonUpdateCHecker.cs | 21 +- .../Local/Cookies/ChromeCookieDecryptor.cs | 20 +- .../Local/Cookies/ChromeCookieManager.cs | 91 + .../Local/IO/V2/NiconicomeDirectoryIO.cs | 11 +- .../Domain/Local/IO/V2/NiconicomeFileIO.cs | 8 + .../Local/LocalFile/CookieJsonLoader.cs | 1 + .../Domain/Local/SQLite/SqliteCookieLoader.cs | 7 +- .../API/Comment/V1/CommentRequestHandler.cs | 125 + .../V1/Error/CommentRequestHandlerError.cs | 17 + .../Comment/V1/Error/CommentRetreiverError.cs | 17 + .../API/Comment/V1/Local/CommentConverter.cs | 37 + .../API/Comment/V1/Local/CommentRetreiver.cs | 100 + .../API/Comment/V1/Local/CommentType.cs | 28 + .../Local/Server/API/NG/V1/NGHandler.cs | 254 + .../Server/API/RegacyHLS/RegacyHLSHandler.cs | 244 + .../API/RegacyHLS/V1/Error/HLSManagerError.cs | 25 + .../V1/Error/RegacyHLSHandlerError.cs | 17 + .../RegacyHLS/V1/SegmentCreator/HLSManager.cs | 163 + .../Resource/V1/Error/ResourceHandlerError.cs | 15 + .../Server/API/Resource/V1/ResourceHandler.cs | 153 + .../V1/Error/VideoInfoHandlerError.cs | 17 + .../API/VideoInfo/V1/Types/JsWatchInfo.cs | 113 + .../API/VideoInfo/V1/VideoInfoHandler.cs | 203 + .../API/Watch/V1/Error/DecryptorError.cs | 15 + .../Watch/V1/Error/PlaylistCreatorError.cs | 15 + .../API/Watch/V1/Error/WatchHandlerError.cs | 21 + .../API/Watch/V1/HLS/AES/AESInfomation.cs | 23 + .../Server/API/Watch/V1/HLS/AES/Decryptor.cs | 75 + .../API/Watch/V1/HLS/PlaylistCreator.cs | 106 + .../V1/LocalFile/LocalFIleInfoHandler.cs | 14 + .../API/Watch/V1/LocalFile/LocalFileInfo.cs | 44 + .../API/Watch/V1/Session/SessionManager.cs | 164 + .../WatchHandlerStringContent.cs | 27 + .../Local/Server/API/Watch/V1/WatchHandler.cs | 478 + .../Domain/Local/Server/Core/IPHandler.cs | 44 + .../Domain/Local/Server/Core/RequestType.cs | 6 + .../Models/Domain/Local/Server/Core/Server.cs | 204 +- .../Domain/Local/Server/Core/ServerError.cs | 2 + .../Domain/Local/Server/Core/UrlHandler.cs | 31 + .../Domain/Local/Settings/SettingInfo.cs | 21 +- .../Domain/Local/Settings/SettingsNames.cs | 16 +- .../Domain/Local/Store/V2/CookieStore.cs | 38 + .../Models/Domain/Network/NetWorkState.cs | 39 + .../Download/General/LocalFileHandler.cs | 21 +- .../Video/V2/Error/WatchSessionError.cs | 2 + .../Video/V2/Integrate/VideoDownloader.cs | 5 +- .../Download/Video/V2/Session/WatchSession.cs | 43 +- .../Download/Video/V3/DMS/HLS/M3U8Node.cs | 52 + .../Download/Video/V3/DMS/HLS/M3U8Parser.cs | 75 + .../Download/Video/V3/DMS/StreamCollection.cs | 44 + .../Download/Video/V3/DMS/StreamInfo.cs | 270 + .../Download/Video/V3/DMS/StreamParser.cs | 113 + .../Video/V3/Error/KeyDownlaoderError.cs | 15 + .../V3/Error/SegmentDirectoryHandlerError.cs | 17 + .../Video/V3/Error/SegmentDownloaderError.cs | 23 + .../Video/V3/Error/SegmentWriterError.cs | 15 + .../Video/V3/Error/StreamJsonHandlerError.cs | 15 + .../Video/V3/Error/StreamParserError.cs | 17 + .../Video/V3/Error/WatchSessionError.cs | 33 + .../V3/External/ExternalDownloaderHandler.cs | 105 + .../Video/V3/Fetch/Key/KeyDownlaoder.cs | 67 + .../Video/V3/Fetch/Key/KeyInfomation.cs | 10 + .../Fetch/Segment/SegmentDLResultContainer.cs | 82 + .../V3/Fetch/Segment/SegmentDownloader.cs | 157 + .../V3/Fetch/Segment/SegmentInfomation.cs | 95 + .../Video/V3/Fetch/Segment/SegmentWriter.cs | 72 + .../StringContent/SegmentDownloaderSC.cs | 15 + .../Video/V3/Integrate/VideoDownloader.cs | 369 + .../Video/V3/Integrate/VideoDownloaderSC.cs | 27 + .../Video/V3/Local/DMS/DMSFileHandler.cs | 81 + .../V3/Local/DMS/SegmentDirectoryHandler.cs | 209 + .../V3/Local/DMS/SegmentDirectoryInfo.cs | 33 + .../V3/Local/StreamJson/StreamJsonHandler.cs | 148 + .../Video/V3/Local/StreamJson/StreamType.cs | 45 + .../Download/Video/V3/Session/WatchSession.cs | 212 + Niconicome/Models/Domain/Niconico/NicoHttp.cs | 50 +- .../Models/Domain/Niconico/NiconicoContext.cs | 177 +- .../Niconico/Remote/V2/Error/SeriesError.cs | 2 + .../Domain/Niconico/UserAuth/CookiInfo.cs | 71 + .../Niconico/Video/Infomations/DmcInfo.cs | 9 +- .../Video/Infomations/DomainVideoInfo.cs | 32 +- .../Domain/Niconico/Watch/DmcDataHandler.cs | 180 - .../Domain/Niconico/Watch/WatchInfohandler.cs | 2 +- .../Domain/Niconico/Watch/WatchSession.cs | 41 +- .../Domain/Niconico/Watch/WatchSessionInfo.cs | 24 +- .../Models/Domain/Playlist/VideoInfo.cs | 7 + .../Utils/BackgroundTask/BackgroundTask.cs | 11 + .../BackgroundTask/BackgroundTaskManager.cs | 190 + Niconicome/Models/Domain/Utils/DIFactory.cs | 97 +- .../Models/Domain/Utils/Error/ErrorHandler.cs | 20 +- .../Models/Domain/Utils/Error/ErrorType.cs | 28 + .../Models/Domain/Utils/PathOrganizer.cs | 1 + Niconicome/Models/Domain/Utils/Utitls.cs | 19 + .../Helper/Result/Generic/AttemptResult.cs | 1 + .../Database/CookieDBHandler.cs | 245 + .../Database/Error/CookieDBHandlerError.cs | 19 + .../Database/LiteDB/TableNames.cs | 2 + .../Infrastructure/Database/Types/Cookie.cs | 31 + .../Infrastructure/IO/WindowsDirectoryIO.cs | 45 + .../IO/WindowsDirectoryIOError.cs | 2 + .../Models/Infrastructure/IO/WindowsFileIO.cs | 35 +- .../Infrastructure/Network/IPHandler.cs | 31 + .../Local/Addon/API/Net/Http/Fetch/Fetch.cs | 6 +- .../Models/Local/Application/StartUp.cs | 50 +- .../Local/External/ExternalAppUtilsV2.cs | 18 +- .../Models/Local/State/BlazorPageManager.cs | 71 +- Niconicome/Models/Local/State/Tab/V1/Tab.cs | 55 + .../Models/Local/State/Tab/V1/TabControler.cs | 61 + Niconicome/Models/Local/Timer/DlTimer.cs | 78 +- .../Download/Actions/PostDownloadActions.cs | 2 +- .../Actions/V2/PostDownloadActionsManager.cs | 92 + .../V2/PostDownloadActionsManagerError.cs | 21 + .../Network/Download/ContentDownloadHelper.cs | 38 +- .../Network/Download/DownloadSettings.cs | 7 + .../Download/DownloadSettingsHandler.cs | 2 + .../Video/VideoModificationManager.cs | 69 + .../Models/Network/Video/ThumbnailUtility.cs | 1 - .../V2/Manager/Helper/LocalVideoLoader.cs | 19 +- .../Utils/InitializeAwaiter/AwaiterNames.cs | 13 - .../InitializeAwaiter/InitializeAwaiter.cs | 121 - .../InitializeAwaiterHandler.cs | 89 - .../Utils/Reactive/Command/BindableCommand.cs | 11 +- Niconicome/Models/Utils/WindowTabHelper.cs | 154 - Niconicome/Niconicome.csproj | 16 +- Niconicome/Properties/Resources.Designer.cs | 10 + Niconicome/Properties/Resources.resx | 3 + .../ViewModels/Login/LoginBrowserViewModel.cs | 39 +- .../ViewModels/Login/LoginWindowViewModel.cs | 249 - .../Mainpage/MainWindowViewModel.cs | 162 +- .../Mainpage/PlayistTreeViewModel.cs | 393 - .../PlaylistTree/PlaylistTreeBehavior.cs | 5 - .../PlaylistTree/PlaylistTreeViewModel.cs | 2 +- .../DownloadTask/ToolbarViewModel.cs | 14 +- .../NetoworkVideoSettingsViewModel.cs | 165 - .../Subwindows/PlaylistEditViewModel.cs | 98 - .../Mainpage/Subwindows/SearchViewModel.cs | 485 - .../Tabs/DownloadSettingsViewModel.cs | 495 - .../Mainpage/Tabs/OutPutViewModel.cs | 79 - .../ViewModels/Mainpage/Tabs/SortViewModel.cs | 178 - .../Mainpage/Tabs/TimerSettingsViewModel.cs | 143 - .../BottomPanel/BottomPanelViewModel.cs | 36 + .../BottomPanel/DownloadSettingsViewModel.cs | 214 + .../VideoList/BottomPanel/OutputViewModel.cs | 78 + .../VideoList/BottomPanel/StateViewModel.cs | 65 + .../BottomPanel/StringContent/TimerVMSC.cs | 21 + .../VideoList/BottomPanel/TimerViewModel.cs | 110 + .../Tabs/VideoList/Pages/IndexViewModel.cs | 10 +- .../VideoList/Pages/MigrationViewModel.cs | 2 +- .../Tabs/VideoList/Pages/PlaylistViewModel.cs | 12 +- .../Tabs/VideoList/Pages/SearchViewModel.cs | 4 +- .../VideoList/Pages/VideoDetailViewModel.cs | 53 +- .../Tabs/VideoList/VideoInfoViewModel.cs | 5 + .../Mainpage/Tabs/VideolistStateViewModel.cs | 90 - .../ViewModels/Mainpage/VideoListViewmodel.cs | 1851 -- .../Setting/Pages/AppinfopageViewmodel.cs | 24 - .../Pages/DebugSettingsPageViewModel.cs | 70 - .../Pages/DownloadSettingPageViewModel.cs | 299 - .../ExternalSoftwareSettingsViewModel.cs | 106 - .../Setting/Pages/FileSettingsViewModel.cs | 114 - .../Pages/GeneralSettingsPageViewModel.cs | 285 - .../Setting/Pages/RestorePageViewModel.cs | 288 - .../String/RestorePageVMStringContent.cs | 38 - .../Pages/String/StylePageVMStringConnent.cs | 29 - .../Setting/Pages/StylePageViewModel.cs | 106 - .../Pages/VideoListSettingsPageViewModel.cs | 123 - .../Setting/V2/Page/DownloadViewModel.cs | 19 + .../Setting/V2/Page/GeneralViewModel.cs | 5 +- .../Setting/V2/Page/SideMenuViewModel.cs | 14 +- Niconicome/ViewModels/Shared/TabViewModel.cs | 71 + Niconicome/Views/AddonPage/MainManager.xaml | 26 - .../Views/AddonPage/MainManager.xaml.cs | 39 - .../Views/DownloadTask/DownloadTask.xaml.cs | 8 + .../Views/DownloadTask/Pages/Toolbar.razor | 8 - Niconicome/Views/EditPlaylist.xaml | 33 - Niconicome/Views/EditPlaylist.xaml.cs | 27 - Niconicome/Views/Loginxaml.xaml | 57 - Niconicome/Views/Loginxaml.xaml.cs | 27 - Niconicome/Views/MainWindow.xaml | 17 +- Niconicome/Views/MainWindow.xaml.cs | 11 + .../BlazorVideoList/MainVideoList.xaml.cs | 10 + .../BlazorVideoList/Pages/BottomPanel.razor | 39 + .../Region/BlazorVideoList/Pages/Index.razor | 2 + .../Pages/Panels/Download.razor | 144 + .../BlazorVideoList/Pages/Panels/Output.razor | 41 + .../BlazorVideoList/Pages/Panels/State.razor | 31 + .../BlazorVideoList/Pages/Panels/Timer.razor | 92 + .../BlazorVideoList/Pages/VideoDetail.razor | 93 +- .../Mainpage/Region/DownloadSettings.xaml | 99 - .../Mainpage/Region/DownloadSettings.xaml.cs | 18 - Niconicome/Views/Mainpage/Region/Output.xaml | 35 - .../Views/Mainpage/Region/Output.xaml.cs | 18 - Niconicome/Views/Mainpage/Region/Tab.xaml | 13 - Niconicome/Views/Mainpage/Region/Tab.xaml.cs | 18 - .../Views/Mainpage/Region/TimerSettings.xaml | 89 - .../Mainpage/Region/TimerSettings.xaml.cs | 31 - .../Views/Mainpage/Region/VideoList.xaml | 315 - .../Views/Mainpage/Region/VideoList.xaml.cs | 19 - .../Mainpage/Region/VideoSortSetting.xaml | 57 - .../Mainpage/Region/VideoSortSetting.xaml.cs | 18 - .../Views/Mainpage/Region/VideolistState.xaml | 37 - .../Mainpage/Region/VideolistState.xaml.cs | 18 - Niconicome/Views/NetworkVideoSettings.xaml | 52 - Niconicome/Views/NetworkVideoSettings.xaml.cs | 27 - Niconicome/Views/SearchPage.xaml | 175 - Niconicome/Views/SearchPage.xaml.cs | 29 - .../Views/Setting/Pages/AppinfoPage.xaml | 291 - .../Views/Setting/Pages/AppinfoPage.xaml.cs | 28 - .../Setting/Pages/DebugSettingsPage.xaml | 70 - .../Setting/Pages/DebugSettingsPage.xaml.cs | 30 - .../Setting/Pages/DownloadSettingPage.xaml | 207 - .../Setting/Pages/DownloadSettingPage.xaml.cs | 30 - Niconicome/Views/Setting/Pages/EmptyPage.xaml | 30 - .../Views/Setting/Pages/EmptyPage.xaml.cs | 28 - .../Pages/ExternalSoftwareSettingsPage.xaml | 152 - .../ExternalSoftwareSettingsPage.xaml.cs | 30 - .../Views/Setting/Pages/FileSettingsPage.xaml | 196 - .../Setting/Pages/FileSettingsPage.xaml.cs | 17 - .../Views/Setting/Pages/GeneralPage.xaml | 38 - .../Views/Setting/Pages/GeneralPage.xaml.cs | 30 - .../Views/Setting/Pages/RestorePage.xaml | 28 - .../Views/Setting/Pages/RestorePage.xaml.cs | 28 - Niconicome/Views/Setting/Pages/StylePage.xaml | 61 - .../Views/Setting/Pages/StylePage.xaml.cs | 30 - .../Setting/Pages/VideoListSettingPage.xaml | 112 - .../Pages/VideoListSettingPage.xaml.cs | 30 - .../Views/Setting/V2/Pages/Download.razor | 52 +- .../Setting/V2/Pages/ExternalSoftware.razor | 1 + .../Views/Setting/V2/SettingPage.xaml.cs | 8 + .../Views/Setting/V2/Shared/SideMenu.razor | 7 - Niconicome/Views/Shared/TabContainer.razor | 35 + Niconicome/Workspaces/LoginPage.cs | 18 - Niconicome/Workspaces/Mainpage.cs | 40 +- Niconicome/wwwroot/css/dltask.css | 2 +- Niconicome/wwwroot/css/index.css | 129 + Niconicome/wwwroot/css/setting.css | 2 +- Niconicome/wwwroot/css/shared.css | 25 + Niconicome/wwwroot/css/videoDetail.css | 571 +- .../script/Pages/VideoList/Index/main.js | 502 +- .../Pages/VideoList/detail/background.js | 33 + .../script/Pages/VideoList/detail/main.js | 20613 ++++++++++++++++ Niconicome/wwwroot/videolist.html | 10 +- .../V2/Local/DMS/DMSFileHandlerUnitTest.cs | 65 + .../Watch/V2/DMS/HLS/M3U8ParserUnitTest.cs | 137 + .../Watch/V2/DMS/StreamInfoUnitTest.cs | 122 + .../Watch/V2/DMS/StreamParserUnitTest.cs | 62 + NiconicomeTest/NiconicomeTest.csproj | 2 +- .../Local/IO/V2/NiconicomeDirectoryIOStub.cs | 5 + .../Local/IO/V2/NiconicomeFileIOStub.cs | 7 + .../Models/Domain/Niconico/NicoHttpStab.cs | 2 +- .../Models/Domain/Playlist/VideoInfoStub.cs | 2 + .../Domain/Utils/Error/ErrorHandlerStub.cs | 8 +- .../InitializeAwaiterHandlerUnitTest.cs | 36 - .../InitializeAwaiterUnitTest.cs | 35 - NiconicomeWeb/.pnp.cjs | 10808 -------- NiconicomeWeb/css/pages/dltask.scss | 2 +- NiconicomeWeb/css/pages/index.scss | 167 + NiconicomeWeb/css/pages/setting.scss | 2 +- NiconicomeWeb/css/pages/shared.scss | 29 + NiconicomeWeb/css/pages/videoDetail.scss | 747 +- NiconicomeWeb/lib/hls.js/config.ts | 685 + .../lib/hls.js/controller/abr-controller.ts | 896 + .../controller/audio-stream-controller.ts | 981 + .../controller/audio-track-controller.ts | 432 + .../controller/base-playlist-controller.ts | 362 + .../controller/base-stream-controller.ts | 1910 ++ .../hls.js/controller/buffer-controller.ts | 1328 + .../controller/buffer-operation-queue.ts | 83 + .../hls.js/controller/cap-level-controller.ts | 320 + .../lib/hls.js/controller/cmcd-controller.ts | 411 + .../controller/content-steering-controller.ts | 605 + .../lib/hls.js/controller/eme-controller.ts | 1321 + .../lib/hls.js/controller/error-controller.ts | 509 + .../lib/hls.js/controller/fps-controller.ts | 146 + .../lib/hls.js/controller/fragment-finders.ts | 187 + .../lib/hls.js/controller/fragment-tracker.ts | 512 + .../lib/hls.js/controller/gap-controller.ts | 399 + .../hls.js/controller/id3-track-controller.ts | 422 + .../hls.js/controller/latency-controller.ts | 260 + .../lib/hls.js/controller/level-controller.ts | 683 + .../hls.js/controller/stream-controller.ts | 1461 ++ .../controller/subtitle-stream-controller.ts | 532 + .../controller/subtitle-track-controller.ts | 573 + .../hls.js/controller/timeline-controller.ts | 793 + NiconicomeWeb/lib/hls.js/crypt/aes-crypto.ts | 32 + .../lib/hls.js/crypt/aes-decryptor.ts | 337 + .../lib/hls.js/crypt/decrypter-aes-mode.ts | 4 + NiconicomeWeb/lib/hls.js/crypt/decrypter.ts | 218 + .../lib/hls.js/crypt/fast-aes-key.ts | 35 + NiconicomeWeb/lib/hls.js/define-plugin.d.ts | 16 + .../lib/hls.js/demux/audio/aacdemuxer.ts | 95 + .../lib/hls.js/demux/audio/ac3-demuxer.ts | 169 + NiconicomeWeb/lib/hls.js/demux/audio/adts.ts | 320 + .../hls.js/demux/audio/base-audio-demuxer.ts | 197 + NiconicomeWeb/lib/hls.js/demux/audio/dolby.ts | 21 + .../lib/hls.js/demux/audio/mp3demuxer.ts | 84 + .../lib/hls.js/demux/audio/mpegaudio.ts | 177 + NiconicomeWeb/lib/hls.js/demux/chunk-cache.ts | 42 + .../lib/hls.js/demux/dummy-demuxed-track.ts | 13 + NiconicomeWeb/lib/hls.js/demux/id3.ts | 411 + .../lib/hls.js/demux/inject-worker.ts | 41 + NiconicomeWeb/lib/hls.js/demux/mp4demuxer.ts | 200 + NiconicomeWeb/lib/hls.js/demux/sample-aes.ts | 199 + .../lib/hls.js/demux/transmuxer-interface.ts | 405 + .../lib/hls.js/demux/transmuxer-worker.ts | 187 + NiconicomeWeb/lib/hls.js/demux/transmuxer.ts | 548 + NiconicomeWeb/lib/hls.js/demux/tsdemuxer.ts | 1029 + .../hls.js/demux/video/avc-video-parser.ts | 429 + .../hls.js/demux/video/base-video-parser.ts | 206 + .../lib/hls.js/demux/video/exp-golomb.ts | 153 + .../hls.js/demux/video/hevc-video-parser.ts | 746 + NiconicomeWeb/lib/hls.js/empty.js | 3 + NiconicomeWeb/lib/hls.js/errors.ts | 90 + NiconicomeWeb/lib/hls.js/events.ts | 413 + NiconicomeWeb/lib/hls.js/exports-default.ts | 3 + NiconicomeWeb/lib/hls.js/exports-named.ts | 59 + NiconicomeWeb/lib/hls.js/hls.ts | 1175 + NiconicomeWeb/lib/hls.js/is-supported.ts | 54 + NiconicomeWeb/lib/hls.js/loader/date-range.ts | 132 + .../lib/hls.js/loader/fragment-loader.ts | 406 + NiconicomeWeb/lib/hls.js/loader/fragment.ts | 320 + NiconicomeWeb/lib/hls.js/loader/key-loader.ts | 358 + .../lib/hls.js/loader/level-details.ts | 155 + NiconicomeWeb/lib/hls.js/loader/level-key.ts | 211 + NiconicomeWeb/lib/hls.js/loader/load-stats.ts | 17 + .../lib/hls.js/loader/m3u8-parser.ts | 915 + .../lib/hls.js/loader/playlist-loader.ts | 715 + NiconicomeWeb/lib/hls.js/polyfills/number.ts | 15 + NiconicomeWeb/lib/hls.js/remux/aac-helper.ts | 81 + .../lib/hls.js/remux/mp4-generator.ts | 1323 + NiconicomeWeb/lib/hls.js/remux/mp4-remuxer.ts | 1247 + .../lib/hls.js/remux/passthrough-remuxer.ts | 303 + NiconicomeWeb/lib/hls.js/task-loop.ts | 130 + NiconicomeWeb/lib/hls.js/types/buffer.ts | 38 + .../lib/hls.js/types/component-api.ts | 20 + NiconicomeWeb/lib/hls.js/types/demuxer.ts | 158 + NiconicomeWeb/lib/hls.js/types/events.ts | 416 + .../lib/hls.js/types/fragment-tracker.ts | 23 + NiconicomeWeb/lib/hls.js/types/general.ts | 6 + NiconicomeWeb/lib/hls.js/types/level.ts | 253 + NiconicomeWeb/lib/hls.js/types/loader.ts | 194 + .../lib/hls.js/types/media-playlist.ts | 81 + NiconicomeWeb/lib/hls.js/types/remuxer.ts | 77 + NiconicomeWeb/lib/hls.js/types/track.ts | 15 + NiconicomeWeb/lib/hls.js/types/transmuxer.ts | 46 + NiconicomeWeb/lib/hls.js/types/tuples.ts | 6 + NiconicomeWeb/lib/hls.js/types/vtt.ts | 9 + NiconicomeWeb/lib/hls.js/utils/attr-list.ts | 106 + .../lib/hls.js/utils/binary-search.ts | 46 + .../lib/hls.js/utils/buffer-helper.ts | 154 + .../lib/hls.js/utils/cea-608-parser.ts | 1426 ++ NiconicomeWeb/lib/hls.js/utils/chunker.ts | 42 + NiconicomeWeb/lib/hls.js/utils/codecs.ts | 240 + NiconicomeWeb/lib/hls.js/utils/cues.ts | 96 + .../lib/hls.js/utils/discontinuities.ts | 189 + .../hls.js/utils/encryption-methods-util.ts | 21 + .../lib/hls.js/utils/error-helper.ts | 80 + .../hls.js/utils/ewma-bandwidth-estimator.ts | 93 + NiconicomeWeb/lib/hls.js/utils/ewma.ts | 43 + .../lib/hls.js/utils/fetch-loader.ts | 322 + NiconicomeWeb/lib/hls.js/utils/global.ts | 2 + NiconicomeWeb/lib/hls.js/utils/hdr.ts | 70 + NiconicomeWeb/lib/hls.js/utils/hex.ts | 20 + .../lib/hls.js/utils/imsc1-ttml-parser.ts | 263 + .../lib/hls.js/utils/keysystem-util.ts | 48 + .../lib/hls.js/utils/level-helper.ts | 495 + NiconicomeWeb/lib/hls.js/utils/logger.ts | 120 + .../hls.js/utils/media-option-attributes.ts | 61 + .../hls.js/utils/mediacapabilities-helper.ts | 194 + .../lib/hls.js/utils/mediakeys-helper.ts | 162 + .../lib/hls.js/utils/mediasource-helper.ts | 17 + NiconicomeWeb/lib/hls.js/utils/mp4-tools.ts | 1391 ++ .../hls.js/utils/numeric-encoding-utils.ts | 26 + .../lib/hls.js/utils/output-filter.ts | 46 + .../lib/hls.js/utils/rendition-helper.ts | 451 + .../lib/hls.js/utils/texttrack-utils.ts | 167 + NiconicomeWeb/lib/hls.js/utils/time-ranges.ts | 17 + .../lib/hls.js/utils/timescale-conversion.ts | 39 + NiconicomeWeb/lib/hls.js/utils/typed-array.ts | 11 + .../lib/hls.js/utils/variable-substitution.ts | 124 + NiconicomeWeb/lib/hls.js/utils/vttcue.ts | 384 + NiconicomeWeb/lib/hls.js/utils/vttparser.ts | 498 + .../lib/hls.js/utils/webvtt-parser.ts | 217 + NiconicomeWeb/lib/hls.js/utils/xhr-loader.ts | 340 + NiconicomeWeb/package.json | 13 - NiconicomeWeb/scripts/videoList.js | 14 - NiconicomeWeb/scripts/videoList.ts | 24 + NiconicomeWeb/scripts/watch.ts | 90 + NiconicomeWeb/src/shared/Collection/unique.ts | 3 + NiconicomeWeb/src/shared/ElementHandler.ts | 77 +- .../src/shared/Extension/Extension.ts | 8 + .../src/shared/Extension/arrayExtension.ts | 18 + .../src/shared/Extension/dateExtension.ts | 33 + NiconicomeWeb/src/shared/StyleHandler.ts | 4 +- .../SelectionHandler/selectionHandler.ts | 2 +- .../src/watch/background/background.ts | 39 + .../src/watch/shared/message/message.ts | 19 + .../watch/ui/comment/drawer/commentDrawer.ts | 613 + .../watch/ui/comment/drawer/module/Comment.ts | 793 + .../watch/ui/comment/drawer/module/config.ts | 18 + .../watch/ui/comment/drawer/module/types.ts | 90 + .../ui/comment/drawer/utils/DataUtils.ts | 58 + .../src/watch/ui/comment/fetch/comment.ts | 12 + .../watch/ui/comment/fetch/commentFetcher.ts | 119 + .../ui/comment/manager/commentManager.ts | 154 + .../ui/comment/manager/managedComment.ts | 12 + .../src/watch/ui/comment/ng/ngDataFethcer.ts | 77 + .../src/watch/ui/comment/ng/ngHandler.ts | 165 + .../watch/ui/componetnts/comment/comment.tsx | 120 + .../ui/componetnts/comment/commentItem.tsx | 40 + .../ui/componetnts/comment/ng/ngInput.tsx | 59 + .../ui/componetnts/comment/ng/ngItem.tsx | 48 + .../ui/componetnts/comment/ng/ngList.tsx | 66 + .../ui/componetnts/playlist/playlist.tsx | 13 + .../ui/componetnts/playlist/playlistItem.tsx | 57 + .../ui/componetnts/shortcut/shortcut.tsx | 44 + .../componetnts/video/controler/controler.tsx | 18 + .../video/controler/generalControler.tsx | 39 + .../ui/componetnts/video/controler/handle.tsx | 13 + .../video/controler/playControler.tsx | 27 + .../ui/componetnts/video/controler/slider.tsx | 15 + .../video/controler/sliderWrapper.tsx | 52 + .../video/controler/timeControler.tsx | 81 + .../video/systemMessage/systemMessage.tsx | 48 + .../video/systemMessage/videoOverflow.tsx | 53 + .../src/watch/ui/componetnts/video/video.tsx | 113 + .../ui/componetnts/video/videoComment.tsx | 117 + .../video/videoInfo/description.tsx | 26 + .../componetnts/video/videoInfo/majorInfo.tsx | 71 + .../ui/componetnts/video/videoInfo/owner.tsx | 37 + .../ui/componetnts/video/videoInfo/tag.tsx | 30 + .../ui/componetnts/video/videoInfo/title.tsx | 5 + .../componetnts/video/videoInfo/videoInfo.tsx | 30 + .../videoContextMenu/contextMenu.tsx | 44 + NiconicomeWeb/src/watch/ui/hooks/useResize.ts | 13 + .../src/watch/ui/jsWatchInfo/videoInfo.ts | 67 + .../watch/ui/jsWatchInfo/watchInfoHandler.ts | 24 + NiconicomeWeb/src/watch/ui/leftContent.tsx | 24 + NiconicomeWeb/src/watch/ui/main.tsx | 72 + NiconicomeWeb/src/watch/ui/state/logger.ts | 149 + .../src/watch/ui/state/videoState.ts | 108 + .../src/watch/ui/video/videoElement.ts | 195 + .../src/watch/ui/video/videoEventHandler.ts | 37 + NiconicomeWeb/yarn.lock | 318 - deno.json | 7 + deno.lock | 167 + 460 files changed, 75708 insertions(+), 21380 deletions(-) create mode 100644 Niconicome/Design/Auth/cookie.pu create mode 100644 Niconicome/Models/Auth/Cookie/NiconicoCookieManager.cs create mode 100644 Niconicome/Models/Auth/Error/ChromeSharedLoginError.cs delete mode 100644 Niconicome/Models/Auth/Session.cs create mode 100644 Niconicome/Models/Auth/StoredCookieLogin.cs create mode 100644 Niconicome/Models/Domain/Local/Cookies/ChromeCookieManager.cs create mode 100644 Niconicome/Models/Domain/Local/Server/API/Comment/V1/CommentRequestHandler.cs create mode 100644 Niconicome/Models/Domain/Local/Server/API/Comment/V1/Error/CommentRequestHandlerError.cs create mode 100644 Niconicome/Models/Domain/Local/Server/API/Comment/V1/Error/CommentRetreiverError.cs create mode 100644 Niconicome/Models/Domain/Local/Server/API/Comment/V1/Local/CommentConverter.cs create mode 100644 Niconicome/Models/Domain/Local/Server/API/Comment/V1/Local/CommentRetreiver.cs create mode 100644 Niconicome/Models/Domain/Local/Server/API/Comment/V1/Local/CommentType.cs create mode 100644 Niconicome/Models/Domain/Local/Server/API/NG/V1/NGHandler.cs create mode 100644 Niconicome/Models/Domain/Local/Server/API/RegacyHLS/RegacyHLSHandler.cs create mode 100644 Niconicome/Models/Domain/Local/Server/API/RegacyHLS/V1/Error/HLSManagerError.cs create mode 100644 Niconicome/Models/Domain/Local/Server/API/RegacyHLS/V1/Error/RegacyHLSHandlerError.cs create mode 100644 Niconicome/Models/Domain/Local/Server/API/RegacyHLS/V1/SegmentCreator/HLSManager.cs create mode 100644 Niconicome/Models/Domain/Local/Server/API/Resource/V1/Error/ResourceHandlerError.cs create mode 100644 Niconicome/Models/Domain/Local/Server/API/Resource/V1/ResourceHandler.cs create mode 100644 Niconicome/Models/Domain/Local/Server/API/VideoInfo/V1/Error/VideoInfoHandlerError.cs create mode 100644 Niconicome/Models/Domain/Local/Server/API/VideoInfo/V1/Types/JsWatchInfo.cs create mode 100644 Niconicome/Models/Domain/Local/Server/API/VideoInfo/V1/VideoInfoHandler.cs create mode 100644 Niconicome/Models/Domain/Local/Server/API/Watch/V1/Error/DecryptorError.cs create mode 100644 Niconicome/Models/Domain/Local/Server/API/Watch/V1/Error/PlaylistCreatorError.cs create mode 100644 Niconicome/Models/Domain/Local/Server/API/Watch/V1/Error/WatchHandlerError.cs create mode 100644 Niconicome/Models/Domain/Local/Server/API/Watch/V1/HLS/AES/AESInfomation.cs create mode 100644 Niconicome/Models/Domain/Local/Server/API/Watch/V1/HLS/AES/Decryptor.cs create mode 100644 Niconicome/Models/Domain/Local/Server/API/Watch/V1/HLS/PlaylistCreator.cs create mode 100644 Niconicome/Models/Domain/Local/Server/API/Watch/V1/LocalFile/LocalFIleInfoHandler.cs create mode 100644 Niconicome/Models/Domain/Local/Server/API/Watch/V1/LocalFile/LocalFileInfo.cs create mode 100644 Niconicome/Models/Domain/Local/Server/API/Watch/V1/Session/SessionManager.cs create mode 100644 Niconicome/Models/Domain/Local/Server/API/Watch/V1/StringContent/WatchHandlerStringContent.cs create mode 100644 Niconicome/Models/Domain/Local/Server/API/Watch/V1/WatchHandler.cs create mode 100644 Niconicome/Models/Domain/Local/Server/Core/IPHandler.cs create mode 100644 Niconicome/Models/Domain/Local/Store/V2/CookieStore.cs create mode 100644 Niconicome/Models/Domain/Network/NetWorkState.cs create mode 100644 Niconicome/Models/Domain/Niconico/Download/Video/V3/DMS/HLS/M3U8Node.cs create mode 100644 Niconicome/Models/Domain/Niconico/Download/Video/V3/DMS/HLS/M3U8Parser.cs create mode 100644 Niconicome/Models/Domain/Niconico/Download/Video/V3/DMS/StreamCollection.cs create mode 100644 Niconicome/Models/Domain/Niconico/Download/Video/V3/DMS/StreamInfo.cs create mode 100644 Niconicome/Models/Domain/Niconico/Download/Video/V3/DMS/StreamParser.cs create mode 100644 Niconicome/Models/Domain/Niconico/Download/Video/V3/Error/KeyDownlaoderError.cs create mode 100644 Niconicome/Models/Domain/Niconico/Download/Video/V3/Error/SegmentDirectoryHandlerError.cs create mode 100644 Niconicome/Models/Domain/Niconico/Download/Video/V3/Error/SegmentDownloaderError.cs create mode 100644 Niconicome/Models/Domain/Niconico/Download/Video/V3/Error/SegmentWriterError.cs create mode 100644 Niconicome/Models/Domain/Niconico/Download/Video/V3/Error/StreamJsonHandlerError.cs create mode 100644 Niconicome/Models/Domain/Niconico/Download/Video/V3/Error/StreamParserError.cs create mode 100644 Niconicome/Models/Domain/Niconico/Download/Video/V3/Error/WatchSessionError.cs create mode 100644 Niconicome/Models/Domain/Niconico/Download/Video/V3/External/ExternalDownloaderHandler.cs create mode 100644 Niconicome/Models/Domain/Niconico/Download/Video/V3/Fetch/Key/KeyDownlaoder.cs create mode 100644 Niconicome/Models/Domain/Niconico/Download/Video/V3/Fetch/Key/KeyInfomation.cs create mode 100644 Niconicome/Models/Domain/Niconico/Download/Video/V3/Fetch/Segment/SegmentDLResultContainer.cs create mode 100644 Niconicome/Models/Domain/Niconico/Download/Video/V3/Fetch/Segment/SegmentDownloader.cs create mode 100644 Niconicome/Models/Domain/Niconico/Download/Video/V3/Fetch/Segment/SegmentInfomation.cs create mode 100644 Niconicome/Models/Domain/Niconico/Download/Video/V3/Fetch/Segment/SegmentWriter.cs create mode 100644 Niconicome/Models/Domain/Niconico/Download/Video/V3/Fetch/Segment/StringContent/SegmentDownloaderSC.cs create mode 100644 Niconicome/Models/Domain/Niconico/Download/Video/V3/Integrate/VideoDownloader.cs create mode 100644 Niconicome/Models/Domain/Niconico/Download/Video/V3/Integrate/VideoDownloaderSC.cs create mode 100644 Niconicome/Models/Domain/Niconico/Download/Video/V3/Local/DMS/DMSFileHandler.cs create mode 100644 Niconicome/Models/Domain/Niconico/Download/Video/V3/Local/DMS/SegmentDirectoryHandler.cs create mode 100644 Niconicome/Models/Domain/Niconico/Download/Video/V3/Local/DMS/SegmentDirectoryInfo.cs create mode 100644 Niconicome/Models/Domain/Niconico/Download/Video/V3/Local/StreamJson/StreamJsonHandler.cs create mode 100644 Niconicome/Models/Domain/Niconico/Download/Video/V3/Local/StreamJson/StreamType.cs create mode 100644 Niconicome/Models/Domain/Niconico/Download/Video/V3/Session/WatchSession.cs create mode 100644 Niconicome/Models/Domain/Niconico/UserAuth/CookiInfo.cs delete mode 100644 Niconicome/Models/Domain/Niconico/Watch/DmcDataHandler.cs create mode 100644 Niconicome/Models/Domain/Utils/BackgroundTask/BackgroundTask.cs create mode 100644 Niconicome/Models/Domain/Utils/BackgroundTask/BackgroundTaskManager.cs create mode 100644 Niconicome/Models/Infrastructure/Database/CookieDBHandler.cs create mode 100644 Niconicome/Models/Infrastructure/Database/Error/CookieDBHandlerError.cs create mode 100644 Niconicome/Models/Infrastructure/Database/Types/Cookie.cs create mode 100644 Niconicome/Models/Infrastructure/Network/IPHandler.cs create mode 100644 Niconicome/Models/Local/State/Tab/V1/Tab.cs create mode 100644 Niconicome/Models/Local/State/Tab/V1/TabControler.cs create mode 100644 Niconicome/Models/Network/Download/Actions/V2/PostDownloadActionsManager.cs create mode 100644 Niconicome/Models/Network/Download/Actions/V2/PostDownloadActionsManagerError.cs create mode 100644 Niconicome/Models/Network/Download/Modification/Video/VideoModificationManager.cs delete mode 100644 Niconicome/Models/Utils/InitializeAwaiter/AwaiterNames.cs delete mode 100644 Niconicome/Models/Utils/InitializeAwaiter/InitializeAwaiter.cs delete mode 100644 Niconicome/Models/Utils/InitializeAwaiter/InitializeAwaiterHandler.cs delete mode 100644 Niconicome/Models/Utils/WindowTabHelper.cs delete mode 100644 Niconicome/ViewModels/Login/LoginWindowViewModel.cs delete mode 100644 Niconicome/ViewModels/Mainpage/PlayistTreeViewModel.cs delete mode 100644 Niconicome/ViewModels/Mainpage/Subwindows/NetoworkVideoSettingsViewModel.cs delete mode 100644 Niconicome/ViewModels/Mainpage/Subwindows/PlaylistEditViewModel.cs delete mode 100644 Niconicome/ViewModels/Mainpage/Subwindows/SearchViewModel.cs delete mode 100644 Niconicome/ViewModels/Mainpage/Tabs/DownloadSettingsViewModel.cs delete mode 100644 Niconicome/ViewModels/Mainpage/Tabs/OutPutViewModel.cs delete mode 100644 Niconicome/ViewModels/Mainpage/Tabs/SortViewModel.cs delete mode 100644 Niconicome/ViewModels/Mainpage/Tabs/TimerSettingsViewModel.cs create mode 100644 Niconicome/ViewModels/Mainpage/Tabs/VideoList/BottomPanel/BottomPanelViewModel.cs create mode 100644 Niconicome/ViewModels/Mainpage/Tabs/VideoList/BottomPanel/DownloadSettingsViewModel.cs create mode 100644 Niconicome/ViewModels/Mainpage/Tabs/VideoList/BottomPanel/OutputViewModel.cs create mode 100644 Niconicome/ViewModels/Mainpage/Tabs/VideoList/BottomPanel/StateViewModel.cs create mode 100644 Niconicome/ViewModels/Mainpage/Tabs/VideoList/BottomPanel/StringContent/TimerVMSC.cs create mode 100644 Niconicome/ViewModels/Mainpage/Tabs/VideoList/BottomPanel/TimerViewModel.cs delete mode 100644 Niconicome/ViewModels/Mainpage/Tabs/VideolistStateViewModel.cs delete mode 100644 Niconicome/ViewModels/Mainpage/VideoListViewmodel.cs delete mode 100644 Niconicome/ViewModels/Setting/Pages/AppinfopageViewmodel.cs delete mode 100644 Niconicome/ViewModels/Setting/Pages/DebugSettingsPageViewModel.cs delete mode 100644 Niconicome/ViewModels/Setting/Pages/DownloadSettingPageViewModel.cs delete mode 100644 Niconicome/ViewModels/Setting/Pages/ExternalSoftwareSettingsViewModel.cs delete mode 100644 Niconicome/ViewModels/Setting/Pages/FileSettingsViewModel.cs delete mode 100644 Niconicome/ViewModels/Setting/Pages/GeneralSettingsPageViewModel.cs delete mode 100644 Niconicome/ViewModels/Setting/Pages/RestorePageViewModel.cs delete mode 100644 Niconicome/ViewModels/Setting/Pages/String/RestorePageVMStringContent.cs delete mode 100644 Niconicome/ViewModels/Setting/Pages/String/StylePageVMStringConnent.cs delete mode 100644 Niconicome/ViewModels/Setting/Pages/StylePageViewModel.cs delete mode 100644 Niconicome/ViewModels/Setting/Pages/VideoListSettingsPageViewModel.cs create mode 100644 Niconicome/ViewModels/Shared/TabViewModel.cs delete mode 100644 Niconicome/Views/AddonPage/MainManager.xaml delete mode 100644 Niconicome/Views/AddonPage/MainManager.xaml.cs delete mode 100644 Niconicome/Views/EditPlaylist.xaml delete mode 100644 Niconicome/Views/EditPlaylist.xaml.cs delete mode 100644 Niconicome/Views/Loginxaml.xaml delete mode 100644 Niconicome/Views/Loginxaml.xaml.cs create mode 100644 Niconicome/Views/Mainpage/Region/BlazorVideoList/Pages/BottomPanel.razor create mode 100644 Niconicome/Views/Mainpage/Region/BlazorVideoList/Pages/Panels/Download.razor create mode 100644 Niconicome/Views/Mainpage/Region/BlazorVideoList/Pages/Panels/Output.razor create mode 100644 Niconicome/Views/Mainpage/Region/BlazorVideoList/Pages/Panels/State.razor create mode 100644 Niconicome/Views/Mainpage/Region/BlazorVideoList/Pages/Panels/Timer.razor delete mode 100644 Niconicome/Views/Mainpage/Region/DownloadSettings.xaml delete mode 100644 Niconicome/Views/Mainpage/Region/DownloadSettings.xaml.cs delete mode 100644 Niconicome/Views/Mainpage/Region/Output.xaml delete mode 100644 Niconicome/Views/Mainpage/Region/Output.xaml.cs delete mode 100644 Niconicome/Views/Mainpage/Region/Tab.xaml delete mode 100644 Niconicome/Views/Mainpage/Region/Tab.xaml.cs delete mode 100644 Niconicome/Views/Mainpage/Region/TimerSettings.xaml delete mode 100644 Niconicome/Views/Mainpage/Region/TimerSettings.xaml.cs delete mode 100644 Niconicome/Views/Mainpage/Region/VideoList.xaml delete mode 100644 Niconicome/Views/Mainpage/Region/VideoList.xaml.cs delete mode 100644 Niconicome/Views/Mainpage/Region/VideoSortSetting.xaml delete mode 100644 Niconicome/Views/Mainpage/Region/VideoSortSetting.xaml.cs delete mode 100644 Niconicome/Views/Mainpage/Region/VideolistState.xaml delete mode 100644 Niconicome/Views/Mainpage/Region/VideolistState.xaml.cs delete mode 100644 Niconicome/Views/NetworkVideoSettings.xaml delete mode 100644 Niconicome/Views/NetworkVideoSettings.xaml.cs delete mode 100644 Niconicome/Views/SearchPage.xaml delete mode 100644 Niconicome/Views/SearchPage.xaml.cs delete mode 100644 Niconicome/Views/Setting/Pages/AppinfoPage.xaml delete mode 100644 Niconicome/Views/Setting/Pages/AppinfoPage.xaml.cs delete mode 100644 Niconicome/Views/Setting/Pages/DebugSettingsPage.xaml delete mode 100644 Niconicome/Views/Setting/Pages/DebugSettingsPage.xaml.cs delete mode 100644 Niconicome/Views/Setting/Pages/DownloadSettingPage.xaml delete mode 100644 Niconicome/Views/Setting/Pages/DownloadSettingPage.xaml.cs delete mode 100644 Niconicome/Views/Setting/Pages/EmptyPage.xaml delete mode 100644 Niconicome/Views/Setting/Pages/EmptyPage.xaml.cs delete mode 100644 Niconicome/Views/Setting/Pages/ExternalSoftwareSettingsPage.xaml delete mode 100644 Niconicome/Views/Setting/Pages/ExternalSoftwareSettingsPage.xaml.cs delete mode 100644 Niconicome/Views/Setting/Pages/FileSettingsPage.xaml delete mode 100644 Niconicome/Views/Setting/Pages/FileSettingsPage.xaml.cs delete mode 100644 Niconicome/Views/Setting/Pages/GeneralPage.xaml delete mode 100644 Niconicome/Views/Setting/Pages/GeneralPage.xaml.cs delete mode 100644 Niconicome/Views/Setting/Pages/RestorePage.xaml delete mode 100644 Niconicome/Views/Setting/Pages/RestorePage.xaml.cs delete mode 100644 Niconicome/Views/Setting/Pages/StylePage.xaml delete mode 100644 Niconicome/Views/Setting/Pages/StylePage.xaml.cs delete mode 100644 Niconicome/Views/Setting/Pages/VideoListSettingPage.xaml delete mode 100644 Niconicome/Views/Setting/Pages/VideoListSettingPage.xaml.cs create mode 100644 Niconicome/Views/Shared/TabContainer.razor delete mode 100644 Niconicome/Workspaces/LoginPage.cs create mode 100644 Niconicome/wwwroot/script/Pages/VideoList/detail/background.js create mode 100644 Niconicome/wwwroot/script/Pages/VideoList/detail/main.js create mode 100644 NiconicomeTest/NetWork/Download/Video/V2/Local/DMS/DMSFileHandlerUnitTest.cs create mode 100644 NiconicomeTest/NetWork/Niconico/Watch/V2/DMS/HLS/M3U8ParserUnitTest.cs create mode 100644 NiconicomeTest/NetWork/Niconico/Watch/V2/DMS/StreamInfoUnitTest.cs create mode 100644 NiconicomeTest/NetWork/Niconico/Watch/V2/DMS/StreamParserUnitTest.cs delete mode 100644 NiconicomeTest/Utils/InitializeAwaiter/InitializeAwaiterHandlerUnitTest.cs delete mode 100644 NiconicomeTest/Utils/InitializeAwaiter/InitializeAwaiterUnitTest.cs delete mode 100644 NiconicomeWeb/.pnp.cjs create mode 100644 NiconicomeWeb/lib/hls.js/config.ts create mode 100644 NiconicomeWeb/lib/hls.js/controller/abr-controller.ts create mode 100644 NiconicomeWeb/lib/hls.js/controller/audio-stream-controller.ts create mode 100644 NiconicomeWeb/lib/hls.js/controller/audio-track-controller.ts create mode 100644 NiconicomeWeb/lib/hls.js/controller/base-playlist-controller.ts create mode 100644 NiconicomeWeb/lib/hls.js/controller/base-stream-controller.ts create mode 100644 NiconicomeWeb/lib/hls.js/controller/buffer-controller.ts create mode 100644 NiconicomeWeb/lib/hls.js/controller/buffer-operation-queue.ts create mode 100644 NiconicomeWeb/lib/hls.js/controller/cap-level-controller.ts create mode 100644 NiconicomeWeb/lib/hls.js/controller/cmcd-controller.ts create mode 100644 NiconicomeWeb/lib/hls.js/controller/content-steering-controller.ts create mode 100644 NiconicomeWeb/lib/hls.js/controller/eme-controller.ts create mode 100644 NiconicomeWeb/lib/hls.js/controller/error-controller.ts create mode 100644 NiconicomeWeb/lib/hls.js/controller/fps-controller.ts create mode 100644 NiconicomeWeb/lib/hls.js/controller/fragment-finders.ts create mode 100644 NiconicomeWeb/lib/hls.js/controller/fragment-tracker.ts create mode 100644 NiconicomeWeb/lib/hls.js/controller/gap-controller.ts create mode 100644 NiconicomeWeb/lib/hls.js/controller/id3-track-controller.ts create mode 100644 NiconicomeWeb/lib/hls.js/controller/latency-controller.ts create mode 100644 NiconicomeWeb/lib/hls.js/controller/level-controller.ts create mode 100644 NiconicomeWeb/lib/hls.js/controller/stream-controller.ts create mode 100644 NiconicomeWeb/lib/hls.js/controller/subtitle-stream-controller.ts create mode 100644 NiconicomeWeb/lib/hls.js/controller/subtitle-track-controller.ts create mode 100644 NiconicomeWeb/lib/hls.js/controller/timeline-controller.ts create mode 100644 NiconicomeWeb/lib/hls.js/crypt/aes-crypto.ts create mode 100644 NiconicomeWeb/lib/hls.js/crypt/aes-decryptor.ts create mode 100644 NiconicomeWeb/lib/hls.js/crypt/decrypter-aes-mode.ts create mode 100644 NiconicomeWeb/lib/hls.js/crypt/decrypter.ts create mode 100644 NiconicomeWeb/lib/hls.js/crypt/fast-aes-key.ts create mode 100644 NiconicomeWeb/lib/hls.js/define-plugin.d.ts create mode 100644 NiconicomeWeb/lib/hls.js/demux/audio/aacdemuxer.ts create mode 100644 NiconicomeWeb/lib/hls.js/demux/audio/ac3-demuxer.ts create mode 100644 NiconicomeWeb/lib/hls.js/demux/audio/adts.ts create mode 100644 NiconicomeWeb/lib/hls.js/demux/audio/base-audio-demuxer.ts create mode 100644 NiconicomeWeb/lib/hls.js/demux/audio/dolby.ts create mode 100644 NiconicomeWeb/lib/hls.js/demux/audio/mp3demuxer.ts create mode 100644 NiconicomeWeb/lib/hls.js/demux/audio/mpegaudio.ts create mode 100644 NiconicomeWeb/lib/hls.js/demux/chunk-cache.ts create mode 100644 NiconicomeWeb/lib/hls.js/demux/dummy-demuxed-track.ts create mode 100644 NiconicomeWeb/lib/hls.js/demux/id3.ts create mode 100644 NiconicomeWeb/lib/hls.js/demux/inject-worker.ts create mode 100644 NiconicomeWeb/lib/hls.js/demux/mp4demuxer.ts create mode 100644 NiconicomeWeb/lib/hls.js/demux/sample-aes.ts create mode 100644 NiconicomeWeb/lib/hls.js/demux/transmuxer-interface.ts create mode 100644 NiconicomeWeb/lib/hls.js/demux/transmuxer-worker.ts create mode 100644 NiconicomeWeb/lib/hls.js/demux/transmuxer.ts create mode 100644 NiconicomeWeb/lib/hls.js/demux/tsdemuxer.ts create mode 100644 NiconicomeWeb/lib/hls.js/demux/video/avc-video-parser.ts create mode 100644 NiconicomeWeb/lib/hls.js/demux/video/base-video-parser.ts create mode 100644 NiconicomeWeb/lib/hls.js/demux/video/exp-golomb.ts create mode 100644 NiconicomeWeb/lib/hls.js/demux/video/hevc-video-parser.ts create mode 100644 NiconicomeWeb/lib/hls.js/empty.js create mode 100644 NiconicomeWeb/lib/hls.js/errors.ts create mode 100644 NiconicomeWeb/lib/hls.js/events.ts create mode 100644 NiconicomeWeb/lib/hls.js/exports-default.ts create mode 100644 NiconicomeWeb/lib/hls.js/exports-named.ts create mode 100644 NiconicomeWeb/lib/hls.js/hls.ts create mode 100644 NiconicomeWeb/lib/hls.js/is-supported.ts create mode 100644 NiconicomeWeb/lib/hls.js/loader/date-range.ts create mode 100644 NiconicomeWeb/lib/hls.js/loader/fragment-loader.ts create mode 100644 NiconicomeWeb/lib/hls.js/loader/fragment.ts create mode 100644 NiconicomeWeb/lib/hls.js/loader/key-loader.ts create mode 100644 NiconicomeWeb/lib/hls.js/loader/level-details.ts create mode 100644 NiconicomeWeb/lib/hls.js/loader/level-key.ts create mode 100644 NiconicomeWeb/lib/hls.js/loader/load-stats.ts create mode 100644 NiconicomeWeb/lib/hls.js/loader/m3u8-parser.ts create mode 100644 NiconicomeWeb/lib/hls.js/loader/playlist-loader.ts create mode 100644 NiconicomeWeb/lib/hls.js/polyfills/number.ts create mode 100644 NiconicomeWeb/lib/hls.js/remux/aac-helper.ts create mode 100644 NiconicomeWeb/lib/hls.js/remux/mp4-generator.ts create mode 100644 NiconicomeWeb/lib/hls.js/remux/mp4-remuxer.ts create mode 100644 NiconicomeWeb/lib/hls.js/remux/passthrough-remuxer.ts create mode 100644 NiconicomeWeb/lib/hls.js/task-loop.ts create mode 100644 NiconicomeWeb/lib/hls.js/types/buffer.ts create mode 100644 NiconicomeWeb/lib/hls.js/types/component-api.ts create mode 100644 NiconicomeWeb/lib/hls.js/types/demuxer.ts create mode 100644 NiconicomeWeb/lib/hls.js/types/events.ts create mode 100644 NiconicomeWeb/lib/hls.js/types/fragment-tracker.ts create mode 100644 NiconicomeWeb/lib/hls.js/types/general.ts create mode 100644 NiconicomeWeb/lib/hls.js/types/level.ts create mode 100644 NiconicomeWeb/lib/hls.js/types/loader.ts create mode 100644 NiconicomeWeb/lib/hls.js/types/media-playlist.ts create mode 100644 NiconicomeWeb/lib/hls.js/types/remuxer.ts create mode 100644 NiconicomeWeb/lib/hls.js/types/track.ts create mode 100644 NiconicomeWeb/lib/hls.js/types/transmuxer.ts create mode 100644 NiconicomeWeb/lib/hls.js/types/tuples.ts create mode 100644 NiconicomeWeb/lib/hls.js/types/vtt.ts create mode 100644 NiconicomeWeb/lib/hls.js/utils/attr-list.ts create mode 100644 NiconicomeWeb/lib/hls.js/utils/binary-search.ts create mode 100644 NiconicomeWeb/lib/hls.js/utils/buffer-helper.ts create mode 100644 NiconicomeWeb/lib/hls.js/utils/cea-608-parser.ts create mode 100644 NiconicomeWeb/lib/hls.js/utils/chunker.ts create mode 100644 NiconicomeWeb/lib/hls.js/utils/codecs.ts create mode 100644 NiconicomeWeb/lib/hls.js/utils/cues.ts create mode 100644 NiconicomeWeb/lib/hls.js/utils/discontinuities.ts create mode 100644 NiconicomeWeb/lib/hls.js/utils/encryption-methods-util.ts create mode 100644 NiconicomeWeb/lib/hls.js/utils/error-helper.ts create mode 100644 NiconicomeWeb/lib/hls.js/utils/ewma-bandwidth-estimator.ts create mode 100644 NiconicomeWeb/lib/hls.js/utils/ewma.ts create mode 100644 NiconicomeWeb/lib/hls.js/utils/fetch-loader.ts create mode 100644 NiconicomeWeb/lib/hls.js/utils/global.ts create mode 100644 NiconicomeWeb/lib/hls.js/utils/hdr.ts create mode 100644 NiconicomeWeb/lib/hls.js/utils/hex.ts create mode 100644 NiconicomeWeb/lib/hls.js/utils/imsc1-ttml-parser.ts create mode 100644 NiconicomeWeb/lib/hls.js/utils/keysystem-util.ts create mode 100644 NiconicomeWeb/lib/hls.js/utils/level-helper.ts create mode 100644 NiconicomeWeb/lib/hls.js/utils/logger.ts create mode 100644 NiconicomeWeb/lib/hls.js/utils/media-option-attributes.ts create mode 100644 NiconicomeWeb/lib/hls.js/utils/mediacapabilities-helper.ts create mode 100644 NiconicomeWeb/lib/hls.js/utils/mediakeys-helper.ts create mode 100644 NiconicomeWeb/lib/hls.js/utils/mediasource-helper.ts create mode 100644 NiconicomeWeb/lib/hls.js/utils/mp4-tools.ts create mode 100644 NiconicomeWeb/lib/hls.js/utils/numeric-encoding-utils.ts create mode 100644 NiconicomeWeb/lib/hls.js/utils/output-filter.ts create mode 100644 NiconicomeWeb/lib/hls.js/utils/rendition-helper.ts create mode 100644 NiconicomeWeb/lib/hls.js/utils/texttrack-utils.ts create mode 100644 NiconicomeWeb/lib/hls.js/utils/time-ranges.ts create mode 100644 NiconicomeWeb/lib/hls.js/utils/timescale-conversion.ts create mode 100644 NiconicomeWeb/lib/hls.js/utils/typed-array.ts create mode 100644 NiconicomeWeb/lib/hls.js/utils/variable-substitution.ts create mode 100644 NiconicomeWeb/lib/hls.js/utils/vttcue.ts create mode 100644 NiconicomeWeb/lib/hls.js/utils/vttparser.ts create mode 100644 NiconicomeWeb/lib/hls.js/utils/webvtt-parser.ts create mode 100644 NiconicomeWeb/lib/hls.js/utils/xhr-loader.ts delete mode 100644 NiconicomeWeb/package.json delete mode 100644 NiconicomeWeb/scripts/videoList.js create mode 100644 NiconicomeWeb/scripts/videoList.ts create mode 100644 NiconicomeWeb/scripts/watch.ts create mode 100644 NiconicomeWeb/src/shared/Collection/unique.ts create mode 100644 NiconicomeWeb/src/shared/Extension/Extension.ts create mode 100644 NiconicomeWeb/src/shared/Extension/arrayExtension.ts create mode 100644 NiconicomeWeb/src/shared/Extension/dateExtension.ts create mode 100644 NiconicomeWeb/src/watch/background/background.ts create mode 100644 NiconicomeWeb/src/watch/shared/message/message.ts create mode 100644 NiconicomeWeb/src/watch/ui/comment/drawer/commentDrawer.ts create mode 100644 NiconicomeWeb/src/watch/ui/comment/drawer/module/Comment.ts create mode 100644 NiconicomeWeb/src/watch/ui/comment/drawer/module/config.ts create mode 100644 NiconicomeWeb/src/watch/ui/comment/drawer/module/types.ts create mode 100644 NiconicomeWeb/src/watch/ui/comment/drawer/utils/DataUtils.ts create mode 100644 NiconicomeWeb/src/watch/ui/comment/fetch/comment.ts create mode 100644 NiconicomeWeb/src/watch/ui/comment/fetch/commentFetcher.ts create mode 100644 NiconicomeWeb/src/watch/ui/comment/manager/commentManager.ts create mode 100644 NiconicomeWeb/src/watch/ui/comment/manager/managedComment.ts create mode 100644 NiconicomeWeb/src/watch/ui/comment/ng/ngDataFethcer.ts create mode 100644 NiconicomeWeb/src/watch/ui/comment/ng/ngHandler.ts create mode 100644 NiconicomeWeb/src/watch/ui/componetnts/comment/comment.tsx create mode 100644 NiconicomeWeb/src/watch/ui/componetnts/comment/commentItem.tsx create mode 100644 NiconicomeWeb/src/watch/ui/componetnts/comment/ng/ngInput.tsx create mode 100644 NiconicomeWeb/src/watch/ui/componetnts/comment/ng/ngItem.tsx create mode 100644 NiconicomeWeb/src/watch/ui/componetnts/comment/ng/ngList.tsx create mode 100644 NiconicomeWeb/src/watch/ui/componetnts/playlist/playlist.tsx create mode 100644 NiconicomeWeb/src/watch/ui/componetnts/playlist/playlistItem.tsx create mode 100644 NiconicomeWeb/src/watch/ui/componetnts/shortcut/shortcut.tsx create mode 100644 NiconicomeWeb/src/watch/ui/componetnts/video/controler/controler.tsx create mode 100644 NiconicomeWeb/src/watch/ui/componetnts/video/controler/generalControler.tsx create mode 100644 NiconicomeWeb/src/watch/ui/componetnts/video/controler/handle.tsx create mode 100644 NiconicomeWeb/src/watch/ui/componetnts/video/controler/playControler.tsx create mode 100644 NiconicomeWeb/src/watch/ui/componetnts/video/controler/slider.tsx create mode 100644 NiconicomeWeb/src/watch/ui/componetnts/video/controler/sliderWrapper.tsx create mode 100644 NiconicomeWeb/src/watch/ui/componetnts/video/controler/timeControler.tsx create mode 100644 NiconicomeWeb/src/watch/ui/componetnts/video/systemMessage/systemMessage.tsx create mode 100644 NiconicomeWeb/src/watch/ui/componetnts/video/systemMessage/videoOverflow.tsx create mode 100644 NiconicomeWeb/src/watch/ui/componetnts/video/video.tsx create mode 100644 NiconicomeWeb/src/watch/ui/componetnts/video/videoComment.tsx create mode 100644 NiconicomeWeb/src/watch/ui/componetnts/video/videoInfo/description.tsx create mode 100644 NiconicomeWeb/src/watch/ui/componetnts/video/videoInfo/majorInfo.tsx create mode 100644 NiconicomeWeb/src/watch/ui/componetnts/video/videoInfo/owner.tsx create mode 100644 NiconicomeWeb/src/watch/ui/componetnts/video/videoInfo/tag.tsx create mode 100644 NiconicomeWeb/src/watch/ui/componetnts/video/videoInfo/title.tsx create mode 100644 NiconicomeWeb/src/watch/ui/componetnts/video/videoInfo/videoInfo.tsx create mode 100644 NiconicomeWeb/src/watch/ui/componetnts/videoContextMenu/contextMenu.tsx create mode 100644 NiconicomeWeb/src/watch/ui/hooks/useResize.ts create mode 100644 NiconicomeWeb/src/watch/ui/jsWatchInfo/videoInfo.ts create mode 100644 NiconicomeWeb/src/watch/ui/jsWatchInfo/watchInfoHandler.ts create mode 100644 NiconicomeWeb/src/watch/ui/leftContent.tsx create mode 100644 NiconicomeWeb/src/watch/ui/main.tsx create mode 100644 NiconicomeWeb/src/watch/ui/state/logger.ts create mode 100644 NiconicomeWeb/src/watch/ui/state/videoState.ts create mode 100644 NiconicomeWeb/src/watch/ui/video/videoElement.ts create mode 100644 NiconicomeWeb/src/watch/ui/video/videoEventHandler.ts delete mode 100644 NiconicomeWeb/yarn.lock create mode 100644 deno.json create mode 100644 deno.lock diff --git a/Niconicome/App.xaml.cs b/Niconicome/App.xaml.cs index 43cd9a1a..3bbf0008 100644 --- a/Niconicome/App.xaml.cs +++ b/Niconicome/App.xaml.cs @@ -43,7 +43,6 @@ protected override Window CreateShell() protected override void RegisterTypes(IContainerRegistry containerRegistry) { containerRegistry.RegisterDialog(nameof(CommonMessageBox)); - containerRegistry.RegisterDialog(nameof(MainManager)); } /// diff --git a/Niconicome/Design/Auth/cookie.pu b/Niconicome/Design/Auth/cookie.pu new file mode 100644 index 00000000..2b100d02 --- /dev/null +++ b/Niconicome/Design/Auth/cookie.pu @@ -0,0 +1,56 @@ +@startuml +hide empty members + +title Cookieローカル保管システム + + +interface ICookieInfo +class CookieInfo +interface ICookieStore +class CookieDBHAndler +interface IAttemptResult +interface INiconicoCookieManager +class NiconicoCookieManager +class WebViewBehavior + +interface IWebview2SharedLogin +interface ICoreWebview2Handler + +class V +class VM + +interface ICookieInfo { + +string UserSession + +string UserSessionSecure + +Task CheckCookie() +} + +interface ICookieStore { + +IAttemptResult GetCookieInfo(); + +IAttemptResult DeleteCookieInfo(); + +IAttemptResult Update(ICookieInfo cookie); +} + +interface INiconicoCookieManager { + +void Wire(ICoreWebview2Handler handler) + +void UnWire() + +void HandleNavigate() + +bool IsLoggedIn() +} + +class NiconicoCookieManager { + -ICoreWebview2Handler handler +} + +ICookieInfo <|.. CookieInfo +ICookieStore <|.. CookieDBHAndler +INiconicoCookieManager <|.. NiconicoCookieManager +ICookieStore o-- ICookieInfo +ICookieStore <.. INiconicoCookieManager + +ICookieStore <.. IWebview2SharedLogin +V *-- WebViewBehavior +INiconicoCookieManager <.. WebViewBehavior + + +@enduml \ No newline at end of file diff --git a/Niconicome/Extensions/System/System.cs b/Niconicome/Extensions/System/System.cs index c164be0a..3ded5f51 100644 --- a/Niconicome/Extensions/System/System.cs +++ b/Niconicome/Extensions/System/System.cs @@ -45,4 +45,13 @@ public static bool IsNullOrEmpty([NotNullWhen(false)]this string? source) return string.IsNullOrEmpty(source); } } + + public static class BoolExtensions + { + //boolの値を逆にする + public static bool Not(this bool source) + { + return !source; + } + } } diff --git a/Niconicome/Main.razor b/Niconicome/Main.razor index 28a3fa91..1c9cde93 100644 --- a/Niconicome/Main.razor +++ b/Niconicome/Main.razor @@ -1,5 +1,6 @@  + diff --git a/Niconicome/Models/Auth/AutoLogin.cs b/Niconicome/Models/Auth/AutoLogin.cs index 2052eada..c4325ea8 100644 --- a/Niconicome/Models/Auth/AutoLogin.cs +++ b/Niconicome/Models/Auth/AutoLogin.cs @@ -9,6 +9,7 @@ using Niconicome.Models.Domain.Local.Settings; using Niconicome.Models.Domain.Local.Store.V2; using Niconicome.Models.Domain.Niconico; +using Niconicome.Models.Domain.Niconico.UserAuth; using Niconicome.Models.Helper.Result; using Niconicome.Models.Local.Settings; @@ -43,21 +44,18 @@ public interface IAutoLogin class AutoLogin : IAutoLogin { - public AutoLogin(ISession session, IAccountManager accountManager, IWebview2SharedLogin webview2SharedLogin, IFirefoxSharedLogin firefoxSharedLogin, IStoreFirefoxSharedLogin storeFirefoxSharedLogin, ISettingsContainer settingsConainer) + public AutoLogin(IWebview2SharedLogin webview2SharedLogin, IFirefoxSharedLogin firefoxSharedLogin, IStoreFirefoxSharedLogin storeFirefoxSharedLogin, ISettingsContainer settingsConainer,IStoredCookieLogin storedCookieLogin) { - this._session = session; - this._accountManager = accountManager; this._webview2SharedLogin = webview2SharedLogin; this._firefoxSharedLogin = firefoxSharedLogin; this._storeFirefoxSharedLogin = storeFirefoxSharedLogin; this._settingsConainer = settingsConainer; + this._storedCookieLogin = storedCookieLogin; } #region field - private readonly ISession _session; - - private readonly IAccountManager _accountManager; + private readonly IStoredCookieLogin _storedCookieLogin; private readonly ISettingsContainer _settingsConainer; @@ -94,12 +92,14 @@ public async Task LoginAsync() bool result = false; var type = this.GetAutoLoginType(); - if (type == AutoLoginType.Normal) + //保存されているCookieでのログインを優先する + if (this._storedCookieLogin.CanLogin()) { - var cred = this._accountManager.GetUserCredential(); - result = await this._session.Login(cred); + result = await this._storedCookieLogin.TryLogin(); + if (result) return true; } - else if (type == AutoLoginType.Webview2) + + if (type == AutoLoginType.Webview2) { result = await this._webview2SharedLogin.TryLogin(); } @@ -122,7 +122,7 @@ public async Task LoginAsync() private AutoLoginType GetAutoLoginType() { - IAttemptResult> mResult = this._settingsConainer.GetSetting(SettingNames.AutoLoginMode, AutoLoginTypeString.Normal); + IAttemptResult> mResult = this._settingsConainer.GetSetting(SettingNames.AutoLoginMode, AutoLoginTypeString.Webview2); IAttemptResult> pResult = this._settingsConainer.GetSetting(SettingNames.FirefoxProfileName, ""); if (!mResult.IsSucceeded || mResult.Data is null) @@ -135,15 +135,10 @@ private AutoLoginType GetAutoLoginType() return AutoLoginType.None; } - var mode = mResult.Data.Value.IsNullOrEmpty() ? AutoLoginTypeString.Normal : mResult.Data.Value; + var mode = mResult.Data.Value.IsNullOrEmpty() ? AutoLoginTypeString.Webview2 : mResult.Data.Value; var ffProfile = pResult.Data.Value; - if (mode == AutoLoginTypeString.Normal) - { - bool isCredencialSaved = this._accountManager.IsPasswordSaved; - if (isCredencialSaved) return AutoLoginType.Normal; - } - else if (mode == AutoLoginTypeString.Webview2) + if (mode == AutoLoginTypeString.Webview2) { bool canLoginWithWebview2 = this._webview2SharedLogin.CanLogin(); if (canLoginWithWebview2) return AutoLoginType.Webview2; @@ -180,7 +175,6 @@ public IEnumerable GetFirefoxProfiles(AutoLoginType loginTy public enum AutoLoginType { None, - Normal, Webview2, Firefox, StoreFirefox, @@ -188,8 +182,6 @@ public enum AutoLoginType static class AutoLoginTypeString { - public const string Normal = "Normal"; - public const string Webview2 = "Webview2"; public const string Firefox = "Firefox"; diff --git a/Niconicome/Models/Auth/Cookie/NiconicoCookieManager.cs b/Niconicome/Models/Auth/Cookie/NiconicoCookieManager.cs new file mode 100644 index 00000000..6da7ba20 --- /dev/null +++ b/Niconicome/Models/Auth/Cookie/NiconicoCookieManager.cs @@ -0,0 +1,106 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Niconicome.Extensions.System; +using Niconicome.Models.Domain.Local.Handlers; +using Niconicome.Models.Domain.Local.Store.V2; +using Niconicome.Models.Domain.Niconico; +using Niconicome.Models.Domain.Niconico.UserAuth; +using Niconicome.Models.Helper.Result; + +namespace Niconicome.Models.Auth.Cookie +{ + interface INiconicoCookieManager + { + /// + /// Webviewをハンドリングする + /// + /// + void Wire(ICoreWebview2Handler handler); + + /// + /// WebViewのハンドリングを解除する + /// + void UnWire(); + + /// + /// URL遷移をハンドリングする + /// + Task HandleNavigate(); + + /// + /// ログイン状態を確認する + /// + /// + Task IsLoggedIn(); + + } + + public class NiconicoCookieManager : INiconicoCookieManager + { + #region field + + private ICoreWebview2Handler? handler; + + private readonly INiconicoContext _context; + + private readonly IStoredCookieLogin _storedCookieLogin; + + #endregion + + public NiconicoCookieManager(INiconicoContext context,IStoredCookieLogin storedCookieLogin) + { + this._context = context; + this._storedCookieLogin = storedCookieLogin; + } + + #region Method + + public void Wire(ICoreWebview2Handler handler) + { + this.handler = handler; + } + + public void UnWire() + { + this.handler = null; + } + + public async Task HandleNavigate() + { + if (this.handler is null) return false; + + var cookies = await this.handler.GetCookiesAsync(@"https://nicovideo.jp"); + + string userSession = string.Empty; + string userSessionSecure = string.Empty; + + foreach (var cookie in cookies) + { + if (cookie.Name == "user_session") + { + userSession = cookie.Value; + } + else if (cookie.Name == "user_session_secure") + { + userSessionSecure = cookie.Value; + } + } + + if (userSession.IsNullOrEmpty() || userSessionSecure.IsNullOrEmpty()) return false; + + var result = await this._context.LoginAndSaveCookieAsync(userSession, userSessionSecure); + + return result.IsSucceeded; + } + + public async Task IsLoggedIn() + { + return this._storedCookieLogin.CanLogin() && await this._storedCookieLogin.TryLogin(); + } + + #endregion + } +} diff --git a/Niconicome/Models/Auth/Error/ChromeSharedLoginError.cs b/Niconicome/Models/Auth/Error/ChromeSharedLoginError.cs new file mode 100644 index 00000000..3e337f05 --- /dev/null +++ b/Niconicome/Models/Auth/Error/ChromeSharedLoginError.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Niconicome.Models.Domain.Utils.Error; + +namespace Niconicome.Models.Auth.Error +{ + public enum ChromeSharedLoginError + { + [ErrorEnum(ErrorLevel.Error, "Google Chromeからのクッキーの取得に失敗しました。")] + FailedToGetCookie, + } +} diff --git a/Niconicome/Models/Auth/FirefoxSharedLogin.cs b/Niconicome/Models/Auth/FirefoxSharedLogin.cs index 3e36c0ca..d9c04d2d 100644 --- a/Niconicome/Models/Auth/FirefoxSharedLogin.cs +++ b/Niconicome/Models/Auth/FirefoxSharedLogin.cs @@ -20,7 +20,7 @@ public interface IFirefoxSharedLogin public class FirefoxSharedLogin : SharedLoginBase, IFirefoxSharedLogin { - public FirefoxSharedLogin(IFirefoxCookieManager firefoxCookieManager, ILogger logger, INicoHttp http, INiconicoContext context, ICookieManager cookieManager, IFirefoxProfileManager firefoxProfileManager) : base(http, cookieManager, context) + public FirefoxSharedLogin(IFirefoxCookieManager firefoxCookieManager, ILogger logger, INiconicoContext context, IFirefoxProfileManager firefoxProfileManager) : base( context) { this.firefoxCookieManager = firefoxCookieManager; this.logger = logger; @@ -60,17 +60,9 @@ public async Task TryLogin(string profileName) if (cookie.UserSession is null || cookie.UserSessionSecure is null) return false; - this.cookieManager.AddCookie("user_session", cookie.UserSession); - this.cookieManager.AddCookie("user_session_secure", cookie.UserSessionSecure); + IAttemptResult result = await this.LoginAndSaveCookieAsync(cookie.UserSession, cookie.UserSessionSecure); - var result = await this.CheckIfLoginSucceeded(); - - if (result) - { - await this.context.RefreshUser(); - } - - return result; + return result.IsSucceeded; } /// diff --git a/Niconicome/Models/Auth/Session.cs b/Niconicome/Models/Auth/Session.cs deleted file mode 100644 index bed1286d..00000000 --- a/Niconicome/Models/Auth/Session.cs +++ /dev/null @@ -1,68 +0,0 @@ -using System; -using System.Threading.Tasks; -using Niconicome.Models.Domain.Niconico; -using Niconicome.ViewModels; -using Reactive.Bindings; -using Reactive.Bindings.Extensions; - -namespace Niconicome.Models.Auth -{ - - public interface ISession - { - Task Login(IUserCredential credential); - Task Logout(); - ReactiveProperty IsLogin { get; } - ReactiveProperty User { get; } - } - - /// - /// ログインセッション - /// - class Session : BindableBase, ISession - { - public Session(INiconicoContext context) - { - this.context = context; - this.IsLogin = new ReactiveProperty(context.IsLogin); - this.User = context.User.ToReactiveProperty().AddTo(this.disposables); - context.User.Subscribe(value => this.IsLogin.Value = value is not null); - } - - #region DI - private readonly INiconicoContext context; - #endregion - - /// - /// ログインフラグ - /// - public ReactiveProperty IsLogin { get; } - - /// - /// ユーザー - /// - public ReactiveProperty User { get; } - - /// ログイン - /// - /// - /// - public async Task Login(IUserCredential credential) - { - if (this.context.IsLogin) return true; - - bool result = await this.context.LoginAsync(credential.Username, credential.Password); - - return result; - } - - /// - /// ログアウト - /// - /// - public async Task Logout() - { - await this.context.LogoutAsync(); - } - } -} diff --git a/Niconicome/Models/Auth/SharedLoginBase.cs b/Niconicome/Models/Auth/SharedLoginBase.cs index ebf468c3..fb1dbcbe 100644 --- a/Niconicome/Models/Auth/SharedLoginBase.cs +++ b/Niconicome/Models/Auth/SharedLoginBase.cs @@ -4,23 +4,18 @@ using System.Text; using System.Threading.Tasks; using Niconicome.Models.Domain.Niconico; +using Niconicome.Models.Helper.Result; namespace Niconicome.Models.Auth { public class SharedLoginBase { - public SharedLoginBase(INicoHttp http, ICookieManager cookieManager, INiconicoContext context) + public SharedLoginBase(INiconicoContext context) { - this.cookieManager = cookieManager; - this.http = http; this.context = context; } #region DIされるクラス - private readonly INicoHttp http; - - protected readonly ICookieManager cookieManager; - protected readonly INiconicoContext context; #endregion @@ -29,16 +24,9 @@ public SharedLoginBase(INicoHttp http, ICookieManager cookieManager, INiconicoCo /// ログインしていない場合、ログインページにリダイレクトされるためLocationヘッダーが指定されるハズ /// /// - protected async Task CheckIfLoginSucceeded() + protected async Task LoginAndSaveCookieAsync(string userSession,string userSessionSecure) { - var result = await this.http.GetAsync(new Uri(@"https://www.nicovideo.jp/my")); - - if (result.Headers.Contains("Location")) - { - return false; - } - - return true; + return await this.context.LoginAndSaveCookieAsync(userSession, userSessionSecure); } } } diff --git a/Niconicome/Models/Auth/StoreFirefoxSharedLogin.cs b/Niconicome/Models/Auth/StoreFirefoxSharedLogin.cs index 5399b55c..84e0a56a 100644 --- a/Niconicome/Models/Auth/StoreFirefoxSharedLogin.cs +++ b/Niconicome/Models/Auth/StoreFirefoxSharedLogin.cs @@ -14,6 +14,6 @@ public interface IStoreFirefoxSharedLogin : IFirefoxSharedLogin { } public class StoreFirefoxSharedLogin : FirefoxSharedLogin, IStoreFirefoxSharedLogin { - public StoreFirefoxSharedLogin(IStoreFirefoxCookieManager firefoxCookie, ILogger logger, INicoHttp http, INiconicoContext context, ICookieManager cookieManager, IStoreFirefoxProfileManager profileManager) : base(firefoxCookie, logger, http, context, cookieManager, profileManager) { } + public StoreFirefoxSharedLogin(IStoreFirefoxCookieManager firefoxCookie, ILogger logger, INiconicoContext context, IStoreFirefoxProfileManager profileManager) : base(firefoxCookie, logger, context, profileManager) { } } } diff --git a/Niconicome/Models/Auth/StoredCookieLogin.cs b/Niconicome/Models/Auth/StoredCookieLogin.cs new file mode 100644 index 00000000..6f4b581c --- /dev/null +++ b/Niconicome/Models/Auth/StoredCookieLogin.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Niconicome.Models.Domain.Local.Cookies; +using Niconicome.Models.Domain.Local.Store.V2; +using Niconicome.Models.Domain.Niconico; +using Niconicome.Models.Domain.Niconico.UserAuth; +using Niconicome.Models.Helper.Result; + +namespace Niconicome.Models.Auth +{ + public interface IStoredCookieLogin + { + /// + /// ログインの可否 + /// + /// + bool CanLogin(); + + /// + /// ログインを試行する + /// + /// + Task TryLogin(); + } + + public class StoredCookieLogin : SharedLoginBase, IStoredCookieLogin + { + public StoredCookieLogin(INiconicoContext context,ICookieStore store) : base(context) + { + this._store = store; + } + + private readonly ICookieStore _store; + + public bool CanLogin() + { + return this._store.Exists(); + } + + public async Task TryLogin() + { + IAttemptResult cookieResult = this._store.GetCookieInfo(); + if (!cookieResult.IsSucceeded||cookieResult.Data is null) + { + return false; + } + + ICookieInfo cookie = cookieResult.Data; + + IAttemptResult result = await this.LoginAndSaveCookieAsync(cookie.UserSession, cookie.UserSessionSecure); + if (!result.IsSucceeded) + { + this._store.DeleteCookieInfo(); + } + + return result.IsSucceeded; + } + + } +} diff --git a/Niconicome/Models/Auth/Webview2SharedLogin.cs b/Niconicome/Models/Auth/Webview2SharedLogin.cs index 865b3802..970c29a9 100644 --- a/Niconicome/Models/Auth/Webview2SharedLogin.cs +++ b/Niconicome/Models/Auth/Webview2SharedLogin.cs @@ -1,6 +1,8 @@ using System; using System.Threading.Tasks; +using System.Windows.Media.Animation; using Niconicome.Models.Domain.Local.Cookies; +using Niconicome.Models.Domain.Local.Store.V2; using Niconicome.Models.Domain.Niconico; using Niconicome.Models.Domain.Utils; @@ -14,16 +16,12 @@ interface IWebview2SharedLogin class Webview2SharedLogin : SharedLoginBase, IWebview2SharedLogin { - public Webview2SharedLogin(IWebview2LocalCookieManager webview2LocalCookieManager, ILogger logger, INicoHttp http, ICookieManager cookieManager, INiconicoContext context) : base(http, cookieManager, context) + public Webview2SharedLogin(INiconicoContext context,IWebview2LocalCookieManager webview2LocalCookieManager) : base(context) { - this.webview2LocalCookieManager = webview2LocalCookieManager; - this.logger = logger; - + this._webview2LocalCookieManager = webview2LocalCookieManager; } - private readonly IWebview2LocalCookieManager webview2LocalCookieManager; - - private readonly ILogger logger; + private readonly IWebview2LocalCookieManager _webview2LocalCookieManager; /// @@ -32,31 +30,21 @@ public Webview2SharedLogin(IWebview2LocalCookieManager webview2LocalCookieManage /// public async Task TryLogin() { - IUserCookieInfo cookie; - - try - { - cookie = this.webview2LocalCookieManager.GetCookieInfo(); - } - catch (Exception e) + if (!this.CanLogin()) { - this.logger.Error("Webview2からのクッキーの取得に失敗しました。", e); return false; } - if (cookie.UserSession is null || cookie.UserSessionSecure is null) return false; + IUserCookieInfo cookieInfo = this._webview2LocalCookieManager.GetCookieInfo(); - this.cookieManager.AddCookie("user_session", cookie.UserSession); - this.cookieManager.AddCookie("user_session_secure", cookie.UserSessionSecure); - - var result = await this.CheckIfLoginSucceeded(); - - if (result) + if (cookieInfo.UserSession is null||cookieInfo.UserSessionSecure is null) { - await this.context.RefreshUser(); + return false; } - return result; + var result = await this.context.LoginAndSaveCookieAsync(cookieInfo.UserSession, cookieInfo.UserSessionSecure); + + return result.IsSucceeded; } /// @@ -65,7 +53,7 @@ public async Task TryLogin() /// public bool CanLogin() { - return this.webview2LocalCookieManager.CanLoginWithWebview2(); + return this._webview2LocalCookieManager.CanLoginWithWebview2(); } } } diff --git a/Niconicome/Models/Const/LocalConstant.cs b/Niconicome/Models/Const/LocalConstant.cs index 907250d9..bb4baa7f 100644 --- a/Niconicome/Models/Const/LocalConstant.cs +++ b/Niconicome/Models/Const/LocalConstant.cs @@ -38,5 +38,7 @@ public static class LocalConstant public const string SettingTabID = "C3C14FDE-B307-4805-BA69-8095E992B75E"; + public const string AppKey = "Niconicome2525++"; + } } diff --git a/Niconicome/Models/Const/NetConstant.cs b/Niconicome/Models/Const/NetConstant.cs index 96d03aac..129754bf 100644 --- a/Niconicome/Models/Const/NetConstant.cs +++ b/Niconicome/Models/Const/NetConstant.cs @@ -46,7 +46,7 @@ class NetConstant /// /// ニコニコのログアウトURL /// - public const string NiconicoLogoutURL = @"https://account.nicovideo.jp/logout"; + public const string NiconicoLogoutURL = @"https://account.nicovideo.jp/logout?site=niconico"; /// /// ニコニコのドメイン @@ -87,5 +87,37 @@ class NetConstant /// ローカルサーバーのデフォルトポート /// public static int DefaultServerPort = 2580; + + /// + /// 視聴アドレス + /// + public static string WatchAddressV1 = "http://localhost:{0}/niconicome/watch/v1/{1}/{2}/main.m3u8"; + + /// + /// 視聴アドレス(HLS) + /// + public static string HLSAddressV1 = "http://localhost:{0}/niconicome/api/regacyhls/v1/{1}/{2}/master.m3u8"; + + /// + /// HLS作成アドレス + /// + public static string HLSCreateAddressV1 = "http://localhost:{0}/niconicome/api/regacyhls/v1/{1}/{2}/create"; + + /// + /// コメントアドレス + /// + public static string CommentAddressV1 = "http://localhost:{0}/niconicome/api/comment/v1/{1}/{2}/comment.json"; + + /// + /// NGAPIアドレス + /// + public static string NGAPIAddressV1 = "http://localhost:{0}/niconicome/api/ng/v1"; + + /// + /// サムネイルアドレス + /// + /// + public static string ThumbnailAddressV1 = "http://localhost:{0}/niconicome/resource/v1/thumb/{1}/thumb.jpg"; + } } diff --git a/Niconicome/Models/Domain/Local/Addons/Core/V2/Update/AddonUpdateCHecker.cs b/Niconicome/Models/Domain/Local/Addons/Core/V2/Update/AddonUpdateCHecker.cs index 15f999c9..7fa691e8 100644 --- a/Niconicome/Models/Domain/Local/Addons/Core/V2/Update/AddonUpdateCHecker.cs +++ b/Niconicome/Models/Domain/Local/Addons/Core/V2/Update/AddonUpdateCHecker.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Net.Http; using System.Text; using System.Threading.Tasks; using Niconicome.Models.Domain.Local.Addons.Core.V2.Engine.Infomation; @@ -19,7 +20,7 @@ public interface IAddonUpdateChecker /// /// /// - Task> CheckForUpdate(IAddonInfomation infomation); + Task> CheckForUpdate(IAddonInfomation infomation, int retry = 0); } public class AddonUpdateChecker : IAddonUpdateChecker @@ -42,14 +43,28 @@ public AddonUpdateChecker(INicoHttp http,ILogger logger,INetWorkHelper netWorkHe #endregion #region Method - public async Task> CheckForUpdate(IAddonInfomation infomation) + public async Task> CheckForUpdate(IAddonInfomation infomation, int retry = 0) { if (!Uri.IsWellFormedUriString(infomation.UpdateJsonURL, UriKind.Absolute)) { return AttemptResult.Fail("URLが不正です。"); } - var res = await this._http.GetAsync(new Uri(infomation.UpdateJsonURL)); + HttpResponseMessage res; + try + { + res = await this._http.GetAsync(new Uri(infomation.UpdateJsonURL)); + } + catch + { + if (retry <= 3) + { + return await this.CheckForUpdate(infomation, retry + 1); + } else + { + return AttemptResult.Fail("アップデート情報の取得に失敗しました。"); + } + } var content = ""; if (res.IsSuccessStatusCode) diff --git a/Niconicome/Models/Domain/Local/Cookies/ChromeCookieDecryptor.cs b/Niconicome/Models/Domain/Local/Cookies/ChromeCookieDecryptor.cs index 675d71ea..a47a8e42 100644 --- a/Niconicome/Models/Domain/Local/Cookies/ChromeCookieDecryptor.cs +++ b/Niconicome/Models/Domain/Local/Cookies/ChromeCookieDecryptor.cs @@ -1,7 +1,11 @@ using System; using System.Security.Cryptography; using System.Text; +using Niconicome.Models.Domain.Niconico.Net.Json.WatchPage.DMC.Request; using Niconicome.Models.Domain.Utils; +using Org.BouncyCastle.Crypto.Engines; +using Org.BouncyCastle.Crypto.Modes; +using Org.BouncyCastle.Crypto.Parameters; namespace Niconicome.Models.Domain.Local.Cookies { @@ -52,15 +56,17 @@ public byte[] GetEncryptionKey(byte[] rawkey) /// public byte[] DecryptCookie(byte[] cipherRaw, byte[] key) { - var cipher = cipherRaw[15..^16]; - var nonce = cipherRaw[3..15]; - var tag = cipherRaw[^16..^0]; - var plainText = new byte[cipher.Length]; - var aes = new AesGcm(key); + var encrypted = cipherRaw[15..]; + var iv = cipherRaw[3..15]; - aes.Decrypt(nonce, cipher, tag, plainText, null); + var cipher = new GcmBlockCipher(new AesEngine()); + var parameters = new AeadParameters(new KeyParameter(key), 128, iv, null); + cipher.Init(false, parameters); + var plainBytes = new byte[cipher.GetOutputSize(encrypted.Length)]; + int retLen = cipher.ProcessBytes(encrypted, 0, encrypted.Length, plainBytes, 0); + cipher.DoFinal(plainBytes, retLen); - return plainText; + return plainBytes; } /// diff --git a/Niconicome/Models/Domain/Local/Cookies/ChromeCookieManager.cs b/Niconicome/Models/Domain/Local/Cookies/ChromeCookieManager.cs new file mode 100644 index 00000000..df30073a --- /dev/null +++ b/Niconicome/Models/Domain/Local/Cookies/ChromeCookieManager.cs @@ -0,0 +1,91 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Niconicome.Models.Domain.Local.LocalFile; +using Niconicome.Models.Domain.Local.SQLite; + +namespace Niconicome.Models.Domain.Local.Cookies +{ + + public interface IChromeCookieManager + { + + /// + /// Cookieが存在するかどうか + /// + /// + bool CanLoginWithChrome(); + + /// + /// Cookieを取得する + /// + /// + IUserCookieInfo GetCookieInfo(); + } + + + public class ChromeCookieManager : IChromeCookieManager + { + public ChromeCookieManager(ICookieJsonLoader cookieJsonLoader, ISqliteCookieLoader sqliteCookieLoader, IChromeCookieDecryptor chromeCookieDecryptor) + { + this.cookieJsonLoader = cookieJsonLoader; + this.sqliteCookieLoader = sqliteCookieLoader; + this.chromeCookieDecryptor = chromeCookieDecryptor; + } + + private readonly ICookieJsonLoader cookieJsonLoader; + + private readonly ISqliteCookieLoader sqliteCookieLoader; + + private readonly IChromeCookieDecryptor chromeCookieDecryptor; + + /// + /// Webview2のcookieを共有可能であるかどうか + /// + /// + public bool CanLoginWithChrome() + { + var cookiePath = this.sqliteCookieLoader.GetCookiePath(CookieType.Chrome); + var jsonPath = this.cookieJsonLoader.GetJsonPath(CookieType.Chrome); + + return File.Exists(cookiePath) && File.Exists(jsonPath); + } + + /// + /// Cookieを取得する + /// + /// + public IUserCookieInfo GetCookieInfo() + { + var cookiePath = this.sqliteCookieLoader.GetCookiePath(CookieType.Chrome); + var jsonPath = this.cookieJsonLoader.GetJsonPath(CookieType.Chrome); + + var cookies = this.sqliteCookieLoader.GetCookies(cookiePath); + var rawKey = this.cookieJsonLoader.GetEncryptedKey(jsonPath); + var key = this.chromeCookieDecryptor.GetEncryptionKey(rawKey); + + if (cookies.UserSession is null || cookies.UserSessionSecure is null) + { + throw new IOException("Cookieの値がnullです。"); + } + + var usersessionEncrypted = this.chromeCookieDecryptor.DecryptCookie(cookies.UserSession, key); + var usersessionSecureEncrypted = this.chromeCookieDecryptor.DecryptCookie(cookies.UserSessionSecure, key); + + var info = new UserCookieInfo() + { + IsUserSessionExpired = cookies.IsUserSessionExpires, + IsUserSessionSecureExpired = cookies.IsUserSessionSecureExpires, + IsNicosidExpired = cookies.IsNnicosidExpires, + UserSession = this.chromeCookieDecryptor.ToUtf8String(usersessionEncrypted), + UserSessionSecure = this.chromeCookieDecryptor.ToUtf8String(usersessionSecureEncrypted), + }; + + return info; + + } + } +} diff --git a/Niconicome/Models/Domain/Local/IO/V2/NiconicomeDirectoryIO.cs b/Niconicome/Models/Domain/Local/IO/V2/NiconicomeDirectoryIO.cs index 9d0469c6..a582492d 100644 --- a/Niconicome/Models/Domain/Local/IO/V2/NiconicomeDirectoryIO.cs +++ b/Niconicome/Models/Domain/Local/IO/V2/NiconicomeDirectoryIO.cs @@ -20,7 +20,7 @@ public interface INiconicomeDirectoryIO /// path配下のディレクトリを取得 /// /// - /// + /// ディレクトリの完全パス一覧 IAttemptResult> GetDirectories(string path); /// @@ -39,6 +39,15 @@ public interface INiconicomeDirectoryIO /// IAttemptResult Delete(string path, bool recursive = true); + /// + /// ディレクトリを移動 + /// + /// + /// + /// + /// + IAttemptResult Move(string source, string destination, bool overwrite = true); + /// /// ディレクトリの存在を確認 /// diff --git a/Niconicome/Models/Domain/Local/IO/V2/NiconicomeFileIO.cs b/Niconicome/Models/Domain/Local/IO/V2/NiconicomeFileIO.cs index bfdb73e2..ddbd8556 100644 --- a/Niconicome/Models/Domain/Local/IO/V2/NiconicomeFileIO.cs +++ b/Niconicome/Models/Domain/Local/IO/V2/NiconicomeFileIO.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; @@ -40,6 +41,13 @@ public interface INiconicomeFileIO /// IAttemptResult Read(string path); + /// + /// ファイルを読み込む + /// + /// + /// + IAttemptResult ReadByte(string path); + /// /// ファイルを削除 /// diff --git a/Niconicome/Models/Domain/Local/LocalFile/CookieJsonLoader.cs b/Niconicome/Models/Domain/Local/LocalFile/CookieJsonLoader.cs index c0aabe22..be77c493 100644 --- a/Niconicome/Models/Domain/Local/LocalFile/CookieJsonLoader.cs +++ b/Niconicome/Models/Domain/Local/LocalFile/CookieJsonLoader.cs @@ -33,6 +33,7 @@ public string GetJsonPath(CookieType type) return type switch { CookieType.Webview2 => @"Niconicome.exe.WebView2\EBWebView\Local State", + CookieType.Chrome => Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"Google\Chrome\User Data\Local State"), _ => throw new InvalidOperationException("そのような種別のcookieには対応していません。") }; } diff --git a/Niconicome/Models/Domain/Local/SQLite/SqliteCookieLoader.cs b/Niconicome/Models/Domain/Local/SQLite/SqliteCookieLoader.cs index bcce1cb5..6957617f 100644 --- a/Niconicome/Models/Domain/Local/SQLite/SqliteCookieLoader.cs +++ b/Niconicome/Models/Domain/Local/SQLite/SqliteCookieLoader.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO; using Niconicome.Models.Domain.Local.External; namespace Niconicome.Models.Domain.Local.SQLite @@ -34,13 +35,14 @@ public string GetCookiePath(CookieType type) return type switch { CookieType.Webview2 => @"Niconicome.exe.WebView2\EBWebView\Default\Network\Cookies", + CookieType.Chrome => Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"Google\Chrome\User Data\Default\Network\Cookies"), _ => throw new InvalidOperationException("そのような種別のCookieには対応していません。"), }; } /// /// 指定したブラウザーの形式に合わせてSQL分を発行しCookieを取得する - /// 対応ブラウザー:Firefox 90, Webview2 + /// 対応ブラウザー:Firefox 90, Webview2, Google Chrome 128 /// /// /// @@ -113,6 +115,7 @@ public class UserCookieRaw : IUserCookieRaw public enum CookieType { Webview2, - Firefox + Firefox, + Chrome, } } diff --git a/Niconicome/Models/Domain/Local/Server/API/Comment/V1/CommentRequestHandler.cs b/Niconicome/Models/Domain/Local/Server/API/Comment/V1/CommentRequestHandler.cs new file mode 100644 index 00000000..4198720f --- /dev/null +++ b/Niconicome/Models/Domain/Local/Server/API/Comment/V1/CommentRequestHandler.cs @@ -0,0 +1,125 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using Niconicome.Models.Domain.Local.Server.API.Comment.V1.Local; +using Niconicome.Models.Domain.Niconico.Net.Json; +using Niconicome.Models.Domain.Playlist; +using Niconicome.Models.Domain.Utils.Error; +using Niconicome.Models.Helper.Result; +using Niconicome.Models.Playlist.V2.Manager; +using Err = Niconicome.Models.Domain.Local.Server.API.Comment.V1.Error.CommentRequestHandlerError; + +namespace Niconicome.Models.Domain.Local.Server.API.Comment.V1 +{ + public interface ICommentRequestHandler + { + /// + /// リクエストを裁く + /// + /// + /// + /// + IAttemptResult Handle(string url, HttpListenerResponse response); + } + + public class CommentRequestHandler : ICommentRequestHandler + { + public CommentRequestHandler(IPlaylistManager playlistManager, IErrorHandler errorHandler, ICommentRetreiver commentRetreiver) + { + this._playlistManager = playlistManager; + this._errorHandler = errorHandler; + this._commentRetreiver = commentRetreiver; + } + + #region field + + private readonly IPlaylistManager _playlistManager; + + private readonly IErrorHandler _errorHandler; + + private readonly ICommentRetreiver _commentRetreiver; + + #endregion + + public IAttemptResult Handle(string url, HttpListenerResponse response) + { + IAttemptResult<(int, string)> playlistAndVideoIDResult = this.GetPlaylistAndVideoID(url); + if (!playlistAndVideoIDResult.IsSucceeded) + { + response.StatusCode = (int)HttpStatusCode.BadRequest; + this.WriteMessage(playlistAndVideoIDResult.Message ?? string.Empty, response.StatusCode, response.OutputStream); + return AttemptResult.Succeeded(); + } + + IAttemptResult playlistResult = this._playlistManager.GetPlaylist(playlistAndVideoIDResult.Data.Item1); + if (!playlistResult.IsSucceeded || playlistResult.Data is null) + { + response.StatusCode = (int)HttpStatusCode.NotFound; + this.WriteMessage(playlistResult.Message ?? string.Empty, response.StatusCode, response.OutputStream); + return AttemptResult.Succeeded(); + } + + IVideoInfo? video = playlistResult.Data.Videos.FirstOrDefault(v => v.NiconicoId == playlistAndVideoIDResult.Data.Item2); + if (video is null) + { + response.StatusCode = (int)HttpStatusCode.NotFound; + this.WriteMessage(this._errorHandler.HandleError(Err.NotFound), response.StatusCode, response.OutputStream); + return AttemptResult.Succeeded(); + } + + IAttemptResult commentResult = this._commentRetreiver.GetComment(playlistAndVideoIDResult.Data.Item2, playlistResult.Data.TemporaryFolderPath); + if (!commentResult.IsSucceeded || commentResult.Data is null) + { + + response.StatusCode = (int)HttpStatusCode.InternalServerError; + this.WriteMessage(commentResult.Message ?? string.Empty, response.StatusCode, response.OutputStream); + return AttemptResult.Succeeded(); + } + + string data = JsonParser.Serialize(commentResult.Data); + byte[] content = Encoding.UTF8.GetBytes(data); + + response.StatusCode = (int)HttpStatusCode.OK; + response.ContentType = "application/json"; + response.OutputStream.Write(content, 0, content.Length); + + return AttemptResult.Succeeded(); + } + + private IAttemptResult<(int, string)> GetPlaylistAndVideoID(string rawURL) + { + var url = new Uri(rawURL); + string[] splited = url.AbsolutePath.TrimStart('/').Split("/"); + + if (splited.Length < 5) + { + return AttemptResult<(int, string)>.Fail(this._errorHandler.HandleError(Err.CannotExtractPlaylistAndVideoID, rawURL)); + } + + return AttemptResult<(int, string)>.Succeeded((int.Parse(splited[4]), splited[5])); + } + + private void WriteMessage(string message, int status, Stream output) + { + var builder = new StringBuilder(); + builder.AppendLine(""); + builder.AppendLine(""); + builder.AppendLine(""); + builder.AppendLine($"Niconicome | {status}"); + builder.AppendLine(""); + builder.AppendLine(""); + builder.AppendLine(""); + builder.AppendLine(message); + builder.AppendLine(""); + builder.AppendLine(""); + + byte[] content = Encoding.UTF8.GetBytes(builder.ToString()); + output.Write(content, 0, content.Length); + } + } +} diff --git a/Niconicome/Models/Domain/Local/Server/API/Comment/V1/Error/CommentRequestHandlerError.cs b/Niconicome/Models/Domain/Local/Server/API/Comment/V1/Error/CommentRequestHandlerError.cs new file mode 100644 index 00000000..a388a9d9 --- /dev/null +++ b/Niconicome/Models/Domain/Local/Server/API/Comment/V1/Error/CommentRequestHandlerError.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Niconicome.Models.Domain.Utils.Error; + +namespace Niconicome.Models.Domain.Local.Server.API.Comment.V1.Error +{ + public enum CommentRequestHandlerError + { + [ErrorEnum(ErrorLevel.Error, "URLからセッションプレイリスト・動画IDを取得できませんでした。(url:{0})")] + CannotExtractPlaylistAndVideoID, + [ErrorEnum(ErrorLevel.Error, "指定されたコンテンツは存在しません。")] + NotFound, + } +} diff --git a/Niconicome/Models/Domain/Local/Server/API/Comment/V1/Error/CommentRetreiverError.cs b/Niconicome/Models/Domain/Local/Server/API/Comment/V1/Error/CommentRetreiverError.cs new file mode 100644 index 00000000..1d9bc554 --- /dev/null +++ b/Niconicome/Models/Domain/Local/Server/API/Comment/V1/Error/CommentRetreiverError.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Niconicome.Models.Domain.Utils.Error; + +namespace Niconicome.Models.Domain.Local.Server.API.Comment.V1.Error +{ + public enum CommentRetreiverError + { + [ErrorEnum(ErrorLevel.Error, "コメントファイルが見つかりませんでした。")] + CommentFileNotFound, + [ErrorEnum(ErrorLevel.Error, "コメントのデシリアライズに失敗しました。")] + FailedToDeserializeComment, + } +} diff --git a/Niconicome/Models/Domain/Local/Server/API/Comment/V1/Local/CommentConverter.cs b/Niconicome/Models/Domain/Local/Server/API/Comment/V1/Local/CommentConverter.cs new file mode 100644 index 00000000..85bc5178 --- /dev/null +++ b/Niconicome/Models/Domain/Local/Server/API/Comment/V1/Local/CommentConverter.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using CXml = Niconicome.Models.Domain.Niconico.Net.Xml.Comment.V2; + + +namespace Niconicome.Models.Domain.Local.Server.API.Comment.V1.Local +{ + public interface ICommentConverter + { + CommentType Convert(CXml::PacketElement packet); + } + + public class CommentConverter : ICommentConverter + { + public CommentType Convert(CXml::PacketElement packet) + { + CommentType commentType = new CommentType(); + foreach (var chat in packet.Chat) + { + Comment comment = new Comment() + { + Body = chat.Text, + UserID = chat.UserId, + VposMS = chat.Vpos * 10, + Mail = chat.Mail, + PostedAt = DateTimeOffset.FromUnixTimeSeconds(chat.Date).ToLocalTime().DateTime, + Number = chat.No, + }; + commentType.Comments.Add(comment); + } + return commentType; + } + } +} diff --git a/Niconicome/Models/Domain/Local/Server/API/Comment/V1/Local/CommentRetreiver.cs b/Niconicome/Models/Domain/Local/Server/API/Comment/V1/Local/CommentRetreiver.cs new file mode 100644 index 00000000..e0c661b4 --- /dev/null +++ b/Niconicome/Models/Domain/Local/Server/API/Comment/V1/Local/CommentRetreiver.cs @@ -0,0 +1,100 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Niconicome.Models.Domain.Local.IO.V2; +using Niconicome.Models.Domain.Local.Server.API.Comment.V1.Error; +using Niconicome.Models.Domain.Niconico.Net.Xml; +using Niconicome.Models.Domain.Utils.Error; +using Niconicome.Models.Helper.Result; +using CXml = Niconicome.Models.Domain.Niconico.Net.Xml.Comment.V2; + +namespace Niconicome.Models.Domain.Local.Server.API.Comment.V1.Local +{ + public interface ICommentRetreiver + { + IAttemptResult GetComment(string niconicoID, string folderPath); + } + + internal class CommentRetreiver : ICommentRetreiver + { + public CommentRetreiver(INiconicomeDirectoryIO directoryIO, INiconicomeFileIO fileIO, IErrorHandler errorHandler, ICommentConverter converter) + { + this._directoryIO = directoryIO; + this._fileIO = fileIO; + this._errorHandler = errorHandler; + this._converter = converter; + } + + #region field + + private readonly INiconicomeDirectoryIO _directoryIO; + + private readonly INiconicomeFileIO _fileIO; + + private readonly IErrorHandler _errorHandler; + + private readonly ICommentConverter _converter; + + #endregion + + public IAttemptResult GetComment(string niconicoID, string folderPath) + { + IAttemptResult commentResult = this.GetXmlComment(niconicoID, folderPath); + if (!commentResult.IsSucceeded || commentResult.Data is null) + { + return AttemptResult.Fail(commentResult.Message); + } + + CommentType converted = this._converter.Convert(commentResult.Data); + + return AttemptResult.Succeeded(converted); + } + + + private IAttemptResult GetXmlComment(string niconicoID, string folderPath) + { + IAttemptResult> fileResult = this._directoryIO.GetFiles(folderPath, "*.xml"); + if (!fileResult.IsSucceeded || fileResult.Data is null) + { + return AttemptResult.Fail(fileResult.Message); + } + + string? targetFile = fileResult.Data.FirstOrDefault(f => f.Contains(niconicoID)); + if (targetFile is null) + { + return AttemptResult.Fail(this._errorHandler.HandleError(CommentRetreiverError.CommentFileNotFound)); + } + + if (!this._fileIO.Exists(targetFile)) + { + return AttemptResult.Fail(this._errorHandler.HandleError(CommentRetreiverError.CommentFileNotFound)); + } + + IAttemptResult fileContentResult = this._fileIO.Read(targetFile); + if (!fileContentResult.IsSucceeded || fileContentResult.Data is null) + { + return AttemptResult.Fail(fileContentResult.Message); + } + + CXml::PacketElement? comment; + + try + { + comment = Xmlparser.Deserialize(fileContentResult.Data); + } + catch (Exception ex) + { + return AttemptResult.Fail(this._errorHandler.HandleError(CommentRetreiverError.FailedToDeserializeComment, ex)); + } + + if (comment is null) + { + return AttemptResult.Fail(this._errorHandler.HandleError(CommentRetreiverError.FailedToDeserializeComment)); + } + + return AttemptResult.Succeeded(comment); + } + } +} diff --git a/Niconicome/Models/Domain/Local/Server/API/Comment/V1/Local/CommentType.cs b/Niconicome/Models/Domain/Local/Server/API/Comment/V1/Local/CommentType.cs new file mode 100644 index 00000000..252d54fe --- /dev/null +++ b/Niconicome/Models/Domain/Local/Server/API/Comment/V1/Local/CommentType.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Niconicome.Models.Domain.Local.Server.API.Comment.V1.Local +{ + public class CommentType + { + public List Comments { get; init; } = new(); + } + + public class Comment + { + public required string Body { get; init; } + + public required string UserID { get; init; } + + public required int VposMS { get; init; } + + public required int Number { get; init; } + + public required string Mail { get; init; } + + public required DateTime PostedAt { get; init; } + } +} diff --git a/Niconicome/Models/Domain/Local/Server/API/NG/V1/NGHandler.cs b/Niconicome/Models/Domain/Local/Server/API/NG/V1/NGHandler.cs new file mode 100644 index 00000000..3e97ac86 --- /dev/null +++ b/Niconicome/Models/Domain/Local/Server/API/NG/V1/NGHandler.cs @@ -0,0 +1,254 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using Niconicome.Extensions.System.List; +using Niconicome.Models.Domain.Local.Settings; +using Niconicome.Models.Domain.Niconico.Net.Json; +using Niconicome.Models.Helper.Result; + +namespace Niconicome.Models.Domain.Local.Server.API.NG.V1 +{ + public interface INGHandler + { + /// + /// リクエストを処理する + /// + /// + /// + /// + IAttemptResult Handle(HttpListenerRequest request, HttpListenerResponse response); + } + + public class NGHandler : INGHandler + { + public NGHandler(ISettingsContainer settingsContainer) + { + this._settingsContainer = settingsContainer; + } + + + private ISettingInfo>? _ngWords; + + private ISettingInfo>? _ngUsers; + + private ISettingInfo>? _ngCommands; + + private readonly ISettingsContainer _settingsContainer; + + public IAttemptResult Handle(HttpListenerRequest request, HttpListenerResponse response) + { + RequestType type = this.GetRequestType(request.Url!.ToString()); + + if (type == RequestType.Get) + { + this.HandleGet(response); + return AttemptResult.Succeeded(); + } + else if (type == RequestType.Set && request.HttpMethod == "POST") + { + this.HandleSet(request, response); + return AttemptResult.Succeeded(); + } + else if (type == RequestType.Delete && request.HttpMethod == "POST") + { + this.HandleDelete(request, response); + return AttemptResult.Succeeded(); + } + else + { + this.WriteMessage($"Invalid request. {request.Url}", (int)HttpStatusCode.BadRequest, response); + return AttemptResult.Succeeded(); + } + } + + private void HandleGet(HttpListenerResponse response) + { + this.SetSettings(); + + var content = JsonParser.Serialize(new { Words = this._ngWords!.Value, Users = this._ngUsers!.Value, Commands = this._ngCommands!.Value }); + var writer = new StreamWriter(response.OutputStream, encoding: Encoding.UTF8); + writer.Write(content); + writer.Flush(); + response.StatusCode = (int)HttpStatusCode.OK; + response.ContentEncoding = Encoding.UTF8; + response.ContentType = "application/json"; + } + + private void HandleSet(HttpListenerRequest request, HttpListenerResponse response) + { + this.SetSettings(); + + var reader = new StreamReader(request.InputStream, encoding: Encoding.UTF8); + string body = reader.ReadToEnd(); + + Request requestObject = JsonParser.DeSerialize(body); + NGType type = this.GetNGType(requestObject); + + if (type == NGType.Word) + { + var ngs = this._ngWords!.Value; + this._ngWords.Value = ngs.AddUnique(requestObject.Value); + } + else if (type == NGType.User) + { + var ngs = this._ngUsers!.Value; + this._ngUsers.Value = ngs.AddUnique(requestObject.Value); + } + else if (type == NGType.Command) + { + var ngs = this._ngCommands!.Value; + this._ngCommands.Value = ngs.AddUnique(requestObject.Value); + } + + Debug.WriteLine(body); + + // Do something + this.WriteMessage("Hello World!!", (int)HttpStatusCode.OK, response); + } + + private void HandleDelete(HttpListenerRequest request, HttpListenerResponse response) + { + this.SetSettings(); + + var reader = new StreamReader(request.InputStream, encoding: Encoding.UTF8); + string body = reader.ReadToEnd(); + + Request requestObject = JsonParser.DeSerialize(body); + NGType type = this.GetNGType(requestObject); + + if (type == NGType.Word) + { + var ngs = this._ngWords!.Value.Where(x => x != requestObject.Value).ToList(); + this._ngWords.Value = ngs; + } + else if (type == NGType.User) + { + var ngs = this._ngUsers!.Value.Where(x => x != requestObject.Value).ToList(); + this._ngUsers.Value = ngs; + } + else if (type == NGType.Command) + { + var ngs = this._ngCommands!.Value.Where(x => x != requestObject.Value).ToList(); + this._ngCommands.Value = ngs; + } + + Debug.WriteLine(body); + + // Do something + this.WriteMessage("{\"status\":200,\"message\":\"success\"}", (int)HttpStatusCode.OK, response); + } + + private RequestType GetRequestType(string url) + { + if (Regex.IsMatch(url, @"https?://.+:\d+/niconicome/api/ng/v1/get/?")) + { + return RequestType.Get; + } + else if (Regex.IsMatch(url, @"https?://.+:\d+/niconicome/api/ng/v1/set/?")) + { + return RequestType.Set; + } + else if (Regex.IsMatch(url, @"https?://.+:\d+/niconicome/api/ng/v1/delete/?")) + { + return RequestType.Delete; + } + + return RequestType.None; + } + + private void WriteMessage(string message, int statusCode, HttpListenerResponse response) + { + var builder = new StringBuilder(); + builder.AppendLine(""); + builder.AppendLine(""); + builder.AppendLine(""); + builder.AppendLine($"Niconicome | {statusCode}"); + builder.AppendLine(""); + builder.AppendLine(""); + builder.AppendLine(""); + builder.AppendLine(message); + builder.AppendLine(""); + builder.AppendLine(""); + + byte[] content = Encoding.UTF8.GetBytes(builder.ToString()); + response.OutputStream.Write(content, 0, content.Length); + response.StatusCode = statusCode; + response.ContentType = "text/html"; + } + + private void SetSettings() + { + if (this._ngWords is null) + { + var result = this._settingsContainer.GetSetting(SettingNames.NGWords, new List()); + if (result.IsSucceeded && result.Data is not null) + { + this._ngWords = result.Data; + } + } + + if (this._ngUsers is null) + { + var result = this._settingsContainer.GetSetting(SettingNames.NGUsers, new List()); + if (result.IsSucceeded && result.Data is not null) + { + this._ngUsers = result.Data; + } + } + + if (this._ngCommands is null) + { + var result = this._settingsContainer.GetSetting(SettingNames.NGCommands, new List()); + if (result.IsSucceeded && result.Data is not null) + { + this._ngCommands = result.Data; + } + } + } + + private NGType GetNGType(Request request) + { + if (request.Type == "user") + { + return NGType.User; + } + else if (request.Type == "command") + { + return NGType.Command; + } + else + { + return NGType.Word; + } + } + + private class Request + { + public string Type { get; set; } = string.Empty; + + public string Value { get; set; } = string.Empty; + } + + private enum NGType + { + Word, + User, + Command, + } + + private enum RequestType + { + Set, + Get, + Delete, + None, + } + } +} diff --git a/Niconicome/Models/Domain/Local/Server/API/RegacyHLS/RegacyHLSHandler.cs b/Niconicome/Models/Domain/Local/Server/API/RegacyHLS/RegacyHLSHandler.cs new file mode 100644 index 00000000..d710ee7f --- /dev/null +++ b/Niconicome/Models/Domain/Local/Server/API/RegacyHLS/RegacyHLSHandler.cs @@ -0,0 +1,244 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using Niconicome.Models.Domain.Local.IO.V2; +using Niconicome.Models.Domain.Local.Server.API.RegacyHLS.V1.SegmentCreator; +using Niconicome.Models.Domain.Utils.Error; +using Niconicome.Models.Helper.Result; +using Err = Niconicome.Models.Domain.Local.Server.API.RegacyHLS.V1.Error.RegacyHLSHandlerError; + +namespace Niconicome.Models.Domain.Local.Server.API.RegacyHLS.V1 +{ + public interface IRegacyHLSHandler + { + /// + /// リクエストを処理 + /// + /// + /// + /// + Task Handle(string url, HttpListenerResponse response, int port); + } + + + public class RegacyHLSHandler : IRegacyHLSHandler + { + public RegacyHLSHandler(IHLSManager hLSManager, INiconicomeFileIO fileIO, IErrorHandler error) + { + this._hLSManager = hLSManager; + this._fileIO = fileIO; + this._error = error; + } + + #region field + + private readonly IHLSManager _hLSManager; + + private readonly INiconicomeFileIO _fileIO; + + private readonly IErrorHandler _error; + + + #endregion + + public async Task Handle(string url, HttpListenerResponse response, int port) + { + RequestType type = this.GetRequestType(url); + if (type == RequestType.Unknown) + { + response.StatusCode = (int)HttpStatusCode.NotFound; + this.WriteMessage(this._error.HandleError(Err.InvalidRequest, url), (int)HttpStatusCode.NotFound, response.OutputStream); + return AttemptResult.Succeeded(); + } + + IAttemptResult<(int, string)> pResult = this.GetPlaylistAndVideoID(url); + if (!pResult.IsSucceeded) + { + response.StatusCode = (int)HttpStatusCode.BadRequest; + this.WriteMessage(pResult.Message ?? string.Empty, (int)HttpStatusCode.BadRequest, response.OutputStream); + return AttemptResult.Succeeded(); + } + + (int playlistID, string niconicoID) = pResult.Data!; + if (type == RequestType.Create) + { + IAttemptResult result = await this._hLSManager.CreateFilesAsync(niconicoID, playlistID); + if (result.IsSucceeded) + { + response.StatusCode = (int)HttpStatusCode.OK; + response.ContentType = "text/html"; + this.WriteMessage($"http://localhost:{port}/niconicome/api/regacyhls/v1/{playlistID}/{niconicoID}/master.m3u8", (int)HttpStatusCode.OK, response.OutputStream); + } + else + { + response.ContentType = "text/html"; + response.StatusCode = (int)HttpStatusCode.InternalServerError; + this.WriteMessage(result.Message ?? string.Empty, (int)HttpStatusCode.InternalServerError, response.OutputStream); + } + } + else if (type == RequestType.M3U8) + { + this.HandleM3U8(response, niconicoID, playlistID.ToString()); + } + else + { + this.HandleTS(response, niconicoID, playlistID.ToString(), Path.GetFileName(url)); + } + + return AttemptResult.Succeeded(); + } + + #region private + + /// + /// マスタープレイリストを返す + /// + /// + /// + /// + /// + private IAttemptResult HandleM3U8(HttpListenerResponse response, string niconicoID, string playlistID) + { + string path = Path.Combine(AppContext.BaseDirectory, "data", "tmp", "hls", playlistID, niconicoID, "playlist.m3u8"); + + if (!this._fileIO.Exists(path)) + { + response.StatusCode = (int)HttpStatusCode.NotFound; + response.ContentType = "text/html"; + this.WriteMessage(this._error.HandleError(Err.FileNotExists, niconicoID, playlistID), (int)HttpStatusCode.NotFound, response.OutputStream); + return AttemptResult.Fail(); + } + + IAttemptResult readResult = this._fileIO.ReadByte(path); + if (!readResult.IsSucceeded || readResult.Data is null) + { + response.StatusCode = (int)HttpStatusCode.InternalServerError; + response.ContentType = "text/html"; + this.WriteMessage(readResult.Message ?? string.Empty, (int)HttpStatusCode.InternalServerError, response.OutputStream); + return AttemptResult.Fail(); + } + + response.ContentType = "application/vnd.apple.mpegurl"; + response.StatusCode = (int)HttpStatusCode.OK; + response.OutputStream.Write(readResult.Data, 0, readResult.Data.Length); + + return AttemptResult.Succeeded(); + + } + + /// + /// セグメントファイルを返す + /// + /// + /// + /// + /// + /// + private IAttemptResult HandleTS(HttpListenerResponse response, string niconicoID, string playlistID, string fileName) + { + string path = Path.Combine(AppContext.BaseDirectory, "data", "tmp", "hls", playlistID, niconicoID, fileName); + + if (!this._fileIO.Exists(path)) + { + response.StatusCode = (int)HttpStatusCode.NotFound; + response.ContentType = "text/html"; + this.WriteMessage(this._error.HandleError(Err.FileNotExists, niconicoID, playlistID), (int)HttpStatusCode.NotFound, response.OutputStream); + return AttemptResult.Fail(); + } + + IAttemptResult readResult = this._fileIO.ReadByte(path); + if (!readResult.IsSucceeded || readResult.Data is null) + { + response.StatusCode = (int)HttpStatusCode.InternalServerError; + response.ContentType = "text/html"; + this.WriteMessage(readResult.Message ?? string.Empty, (int)HttpStatusCode.InternalServerError, response.OutputStream); + return AttemptResult.Fail(); + } + + response.ContentType = "video/MP2T"; + response.StatusCode = (int)HttpStatusCode.OK; + response.OutputStream.Write(readResult.Data, 0, readResult.Data.Length); + + return AttemptResult.Succeeded(); + } + + private IAttemptResult<(int, string)> GetPlaylistAndVideoID(string rawURL) + { + var url = new Uri(rawURL); + string[] splited = url.AbsolutePath.TrimStart('/').Split("/"); + + if (splited.Length < 5) + { + return AttemptResult<(int, string)>.Fail(this._error.HandleError(Err.InvalidRequest, rawURL)); + } + + return AttemptResult<(int, string)>.Succeeded((int.Parse(splited[4]), splited[5])); + } + + + /// + /// レスポンスを書き込む + /// + /// + /// + /// + private void WriteMessage(string message, int status, Stream output) + { + var builder = new StringBuilder(); + builder.AppendLine(""); + builder.AppendLine(""); + builder.AppendLine(""); + builder.AppendLine($"Niconicome | {status}"); + builder.AppendLine(""); + builder.AppendLine(""); + builder.AppendLine(""); + builder.AppendLine(message); + builder.AppendLine(""); + builder.AppendLine(""); + + byte[] content = Encoding.UTF8.GetBytes(builder.ToString()); + output.Write(content, 0, content.Length); + } + + /// + /// リクエストを判別 + /// + /// + /// + private RequestType GetRequestType(string url) + { + if (Regex.IsMatch(url, @"https?://.+:\d+/niconicome/api/regacyhls/v1/\d+/.+/create")) + { + return RequestType.Create; + } + else if (Regex.IsMatch(url, @"https?://.+:\d+/niconicome/api/regacyhls/v1/\d+/.+/master\.m3u8")) + { + return RequestType.M3U8; + } + else if (Regex.IsMatch(url, @"https?://.+:\d+/niconicome/api/regacyhls/v1/\d+/.+/.+\.ts")) + { + return RequestType.TS; + } + else + { + return RequestType.Unknown; + } + } + + + private enum RequestType + { + Unknown, + Create, + TS, + M3U8, + } + + #endregion + } +} diff --git a/Niconicome/Models/Domain/Local/Server/API/RegacyHLS/V1/Error/HLSManagerError.cs b/Niconicome/Models/Domain/Local/Server/API/RegacyHLS/V1/Error/HLSManagerError.cs new file mode 100644 index 00000000..01588d68 --- /dev/null +++ b/Niconicome/Models/Domain/Local/Server/API/RegacyHLS/V1/Error/HLSManagerError.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Niconicome.Models.Domain.Utils.Error; + +namespace Niconicome.Models.Domain.Local.Server.API.RegacyHLS.V1.Error +{ + public enum HLSManagerError + { + [ErrorEnum(ErrorLevel.Error, "動画がダウンロードされていません。(id:{0}, playlist:{1})")] + VideoIsNotDownloaded, + [ErrorEnum(ErrorLevel.Error, "動画ファイルが存在しません。(path:{0})")] + FileDoesNotExist, + [ErrorEnum(ErrorLevel.Error, "ファイルのHLS化に失敗しました。")] + FailedToEncodeFileToHLS, + [ErrorEnum(ErrorLevel.Error, "プレイリストと動画IDを抽出できませんでした。")] + CannotExtractPlaylistAndVideoID, + [ErrorEnum(ErrorLevel.Error, "新サーバーの動画です。(id:{0}, playlist:{1})")] + VideoIsDMS, + [ErrorEnum(ErrorLevel.Error, "現在変換を実行中です。")] + AlreadyRunning, + } +} diff --git a/Niconicome/Models/Domain/Local/Server/API/RegacyHLS/V1/Error/RegacyHLSHandlerError.cs b/Niconicome/Models/Domain/Local/Server/API/RegacyHLS/V1/Error/RegacyHLSHandlerError.cs new file mode 100644 index 00000000..bdcc12b8 --- /dev/null +++ b/Niconicome/Models/Domain/Local/Server/API/RegacyHLS/V1/Error/RegacyHLSHandlerError.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Niconicome.Models.Domain.Utils.Error; + +namespace Niconicome.Models.Domain.Local.Server.API.RegacyHLS.V1.Error +{ + public enum RegacyHLSHandlerError + { + [ErrorEnum(ErrorLevel.Error,"指定されたHLS配信ファイルは生成されていません。(niconicoID:{0}, playlistID:{1})")] + FileNotExists, + [ErrorEnum(ErrorLevel.Error,"不正なリクエストです。(url:{0})")] + InvalidRequest, + } +} diff --git a/Niconicome/Models/Domain/Local/Server/API/RegacyHLS/V1/SegmentCreator/HLSManager.cs b/Niconicome/Models/Domain/Local/Server/API/RegacyHLS/V1/SegmentCreator/HLSManager.cs new file mode 100644 index 00000000..6cbccea6 --- /dev/null +++ b/Niconicome/Models/Domain/Local/Server/API/RegacyHLS/V1/SegmentCreator/HLSManager.cs @@ -0,0 +1,163 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Niconicome.Models.Const; +using Niconicome.Models.Domain.Local.External.Software.FFmpeg.FFmpeg; +using Niconicome.Models.Domain.Local.IO.V2; +using Niconicome.Models.Domain.Local.Server.API.RegacyHLS.V1.Error; +using Niconicome.Models.Domain.Local.Settings; +using Niconicome.Models.Domain.Local.Store.V2; +using Niconicome.Models.Domain.Playlist; +using Niconicome.Models.Domain.Utils.Error; +using Niconicome.Models.Helper.Result; + +namespace Niconicome.Models.Domain.Local.Server.API.RegacyHLS.V1.SegmentCreator +{ + public interface IHLSManager + { + /// + /// インデックスファイルとセグメントファイルを生成する + /// + /// + /// + /// + Task CreateFilesAsync(string niconicoID, int playlistID); + } + + public class HLSManager : IHLSManager + { + public HLSManager(INiconicomeDirectoryIO directoryIO, INiconicomeFileIO fileIO, ISettingsContainer settingsContainer, IErrorHandler errorHandler, IVideoStore store, IFFmpegManager ffmpegManager) + { + this._directoryIO = directoryIO; + this._fileIO = fileIO; + this._errorHandler = errorHandler; + this._settingsContainer = settingsContainer; + this._store = store; + this._fFmpegManager = ffmpegManager; + } + + #region field + + private readonly INiconicomeDirectoryIO _directoryIO; + + private readonly INiconicomeFileIO _fileIO; + + private readonly ISettingsContainer _settingsContainer; + + private readonly IErrorHandler _errorHandler; + + private readonly IVideoStore _store; + + private readonly IFFmpegManager _fFmpegManager; + + private bool _isRunnning; + + #endregion + + #region Method + + public async Task CreateFilesAsync(string niconicoID, int playlistID) + { + if (this._isRunnning) return AttemptResult.Fail(this._errorHandler.HandleError(HLSManagerError.AlreadyRunning)); + + string path = Path.Combine(AppContext.BaseDirectory, "data", "tmp", "hls", playlistID.ToString(), niconicoID, "master.m3u8"); + if (this._fileIO.Exists(path)) + { + return AttemptResult.Succeeded(); + } + + + IAttemptResult vResult = this._store.GetVideo(niconicoID, playlistID); + if (!vResult.IsSucceeded || vResult.Data is null) + { + return AttemptResult.Fail(vResult.Message); + } + + IVideoInfo video = vResult.Data; + if (!video.IsDownloaded.Value) + { + this._errorHandler.HandleError(HLSManagerError.VideoIsNotDownloaded, niconicoID, playlistID); + return AttemptResult.Fail(this._errorHandler.GetMessageForResult(HLSManagerError.VideoIsNotDownloaded, niconicoID, playlistID)); + } + + if (video.FilePath.EndsWith(".json")) + { + return AttemptResult.Fail(this._errorHandler.HandleError(HLSManagerError.VideoIsDMS, niconicoID, playlistID)); + } + + + IAttemptResult dResult = this.CreateDirectory(playlistID, niconicoID); + if (!dResult.IsSucceeded) + { + return dResult; + } + + _isRunnning = true; + var result = await this.CreateHLSFilesAsync(video.FilePath, playlistID, niconicoID); + _isRunnning = false; + + return result; + } + + + #endregion + + #region private + + /// + /// ディレクトリを作成 + /// + /// + private IAttemptResult CreateDirectory(int playlistID, string niconicoID) + { + string path = Path.Combine(AppContext.BaseDirectory, "data", "tmp", "hls", playlistID.ToString(), niconicoID); + + if (this._directoryIO.Exists(path)) + { + IAttemptResult delResult = this._directoryIO.Delete(path); + if (!delResult.IsSucceeded) + { + return delResult; + } + } + + return this._directoryIO.CreateDirectory(path); + } + + + /// + /// playlist.m3u8とtsファイルを作成 + /// + /// + /// + private async Task CreateHLSFilesAsync(string videoFilePath, int playlistID, string niconicoID) + { + if (!this._fileIO.Exists(videoFilePath)) + { + this._errorHandler.HandleError(HLSManagerError.FileDoesNotExist, videoFilePath); + return AttemptResult.Fail(this._errorHandler.GetMessageForResult(HLSManagerError.FileDoesNotExist, videoFilePath)); + } + + IAttemptResult> ffmpeg = this._settingsContainer.GetSetting(SettingNames.FFmpegPath, Format.FFmpegPath); + if (!ffmpeg.IsSucceeded || ffmpeg.Data is null) + { + return AttemptResult.Fail(ffmpeg.Message); + } + + string output = Path.Combine(AppContext.BaseDirectory, "data", "tmp", "hls", playlistID.ToString(), niconicoID); + var command = $"-i \"\" -c:v copy -c:a copy -f hls -hls_time 9 -hls_playlist_type vod -hls_segment_filename \"{output}\\video%3d.ts\" \"\""; + + IAttemptResult result = await this._fFmpegManager.EncodeAsync(videoFilePath, Path.Join(output, "playlist.m3u8"), command, CancellationToken.None); + + if (!result.IsSucceeded) + { + return AttemptResult.Fail(this._errorHandler.GetMessageForResult(HLSManagerError.FailedToEncodeFileToHLS)); + } + + return AttemptResult.Succeeded(); + } + + #endregion + } +} diff --git a/Niconicome/Models/Domain/Local/Server/API/Resource/V1/Error/ResourceHandlerError.cs b/Niconicome/Models/Domain/Local/Server/API/Resource/V1/Error/ResourceHandlerError.cs new file mode 100644 index 00000000..f526f723 --- /dev/null +++ b/Niconicome/Models/Domain/Local/Server/API/Resource/V1/Error/ResourceHandlerError.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Niconicome.Models.Domain.Utils.Error; + +namespace Niconicome.Models.Domain.Local.Server.API.Resource.V1.Error +{ + public enum ResourceHandlerError + { + [ErrorEnum(ErrorLevel.Error, "不正なリクエストです。(url:{0})")] + InvalidUrl, + } +} diff --git a/Niconicome/Models/Domain/Local/Server/API/Resource/V1/ResourceHandler.cs b/Niconicome/Models/Domain/Local/Server/API/Resource/V1/ResourceHandler.cs new file mode 100644 index 00000000..b8062fec --- /dev/null +++ b/Niconicome/Models/Domain/Local/Server/API/Resource/V1/ResourceHandler.cs @@ -0,0 +1,153 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Text; +using System.Threading.Tasks; +using Niconicome.Models.Domain.Local.IO.V2; +using Niconicome.Models.Domain.Utils.StringHandler; +using Niconicome.Models.Helper.Result; +using Niconicome.Models.Network.Video; +using Err = Niconicome.Models.Domain.Local.Server.API.Resource.V1.Error.ResourceHandlerError; +using Niconicome.Models.Domain.Utils.Error; +using System.Text.RegularExpressions; +using System.Drawing; + +namespace Niconicome.Models.Domain.Local.Server.API.Resource.V1 +{ + public interface IResourceHandler + { + /// + /// リクエストを処理 + /// + /// + /// + /// + IAttemptResult Handle(string url, HttpListenerResponse response); + } + + public class ResourceHandler(IThumbnailUtility thumbnailUtility, INiconicomeFileIO fileIO, IErrorHandler errorHandler) : IResourceHandler + { + #region field + + private readonly IThumbnailUtility _thumbnailUtility = thumbnailUtility; + + private readonly INiconicomeFileIO _fileIO = fileIO; + + private readonly IErrorHandler _errorHandler = errorHandler; + + #endregion + + public IAttemptResult Handle(string url, HttpListenerResponse response) + { + RequestType type = this.GetRequestType(url); + if (type == RequestType.None) + { + this.WriteMessage(this._errorHandler.HandleError(Err.InvalidUrl, url), (int)HttpStatusCode.BadRequest, response); + return AttemptResult.Succeeded(); + } + + if (type == RequestType.Thumbnail) + { + this.HandleThumb(url, response); + return AttemptResult.Succeeded(); + } else if (type == RequestType.Favicon) + { + this.HandleFavicon(response); + return AttemptResult.Succeeded(); + } + + return AttemptResult.Succeeded(); + } + + + #region private + + private void HandleThumb(string url, HttpListenerResponse response) + { + + string[] splitted = url.Split('/'); + string niconicoID = splitted[^2]; + string path; + + var thumbnail = this._thumbnailUtility.GetThumbPath(niconicoID); + if (!thumbnail.IsSucceeded || thumbnail.Data is null) + { + path = this._thumbnailUtility.GetDeletedVideoThumb(); + } + else if (!File.Exists(thumbnail.Data)) + { + path = this._thumbnailUtility.GetDeletedVideoThumb(); + } + else + { + path = thumbnail.Data; + } + + IAttemptResult readResult = this._fileIO.ReadByte(path); + if (!readResult.IsSucceeded) + { + this.WriteMessage(readResult.Message ?? string.Empty, (int)HttpStatusCode.InternalServerError, response); + return; + } + + byte[] content = readResult.Data!; + response.OutputStream.Write(content, 0, content.Length); + response.StatusCode = (int)HttpStatusCode.OK; + response.ContentType = "image/jpeg"; + return; + } + + private void HandleFavicon(HttpListenerResponse response) + { + response.ContentType = "image/x-icon"; + Icon icon = Properties.Resources.favicon; + icon.Save(response.OutputStream); + } + + private void WriteMessage(string message, int statusCode, HttpListenerResponse response) + { + var builder = new StringBuilder(); + builder.AppendLine(""); + builder.AppendLine(""); + builder.AppendLine(""); + builder.AppendLine($"Niconicome | {statusCode}"); + builder.AppendLine(""); + builder.AppendLine(""); + builder.AppendLine(""); + builder.AppendLine(message); + builder.AppendLine(""); + builder.AppendLine(""); + + byte[] content = Encoding.UTF8.GetBytes(builder.ToString()); + response.OutputStream.Write(content, 0, content.Length); + response.StatusCode = statusCode; + response.ContentType = "text/html"; + } + + private RequestType GetRequestType(string url) + { + if (Regex.IsMatch(url, @"https?://.+:\d+/niconicome/resource/v1/thumb/.+/thumb\.jpg")) + { + return RequestType.Thumbnail; + } else if (Regex.IsMatch(url, @"https?://.+:\d+/favicon\.ico")) + { + return RequestType.Favicon; + } + + return RequestType.None; + } + + + + #endregion + + private enum RequestType + { + None, + Thumbnail, + Favicon, + } + } +} diff --git a/Niconicome/Models/Domain/Local/Server/API/VideoInfo/V1/Error/VideoInfoHandlerError.cs b/Niconicome/Models/Domain/Local/Server/API/VideoInfo/V1/Error/VideoInfoHandlerError.cs new file mode 100644 index 00000000..66d0d5f1 --- /dev/null +++ b/Niconicome/Models/Domain/Local/Server/API/VideoInfo/V1/Error/VideoInfoHandlerError.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Niconicome.Models.Domain.Utils.Error; + +namespace Niconicome.Models.Domain.Local.Server.API.VideoInfo.V1.Error +{ + public enum VideoInfoHandlerError + { + [ErrorEnum(ErrorLevel.Error,"指定された動画がプレイリストに存在しません。(playlistID:{0}, niconicoID:{1})")] + VideoNotFound, + [ErrorEnum(ErrorLevel.Error,"不正なリクエストです。(url:{0})")] + InvalidRequest, + } +} diff --git a/Niconicome/Models/Domain/Local/Server/API/VideoInfo/V1/Types/JsWatchInfo.cs b/Niconicome/Models/Domain/Local/Server/API/VideoInfo/V1/Types/JsWatchInfo.cs new file mode 100644 index 00000000..c5ac4dab --- /dev/null +++ b/Niconicome/Models/Domain/Local/Server/API/VideoInfo/V1/Types/JsWatchInfo.cs @@ -0,0 +1,113 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json.Serialization; +using System.Threading.Tasks; + +namespace Niconicome.Models.Domain.Local.Server.API.VideoInfo.V1.Types +{ + public class JsWatchInfo + { + public Meta Meta { get; set; } = new(); + + public API API { get; set; } = new(); + + public Media Media { get; set; } = new(); + + public Comment Comment { get; set; } = new(); + + public Thumbnail Thumbnail { get; set; } = new(); + + public Video Video { get; set; } = new(); + + public List PlaylistVideos { get; set; } = new(); + } + + public class Meta + { + public int Status { get; set; } + + public string Message { get; set; } = string.Empty; + } + + public class Media + { + public bool IsDownloaded { get; set; } + + public bool IsDMS { get; set; } + + public string ContentUrl { get; set; } = string.Empty; + + public string CreateUrl { get; set; } = string.Empty; + } + + public class Comment + { + public string ContentUrl { get; set; } = string.Empty; + + + public string CommentNGAPIBaseUrl { get; set; } = string.Empty; + } + + public class Thumbnail + { + public string ContentUrl { get; set; } = string.Empty; + } + + public class PlaylistVideo + { + public string Title { get; set; } = string.Empty; + + public DateTime UploadedAt { get; set; } + + public string NiconicoID { get; set; } = string.Empty; + + public string ThumbnailURL { get; set; } = string.Empty; + + public int Duration { get; set; } + + public int ViewCount { get; set; } + } + + public class Video : PlaylistVideo + { + public List Tags { get; set; } = new(); + + public Owner Owner { get; set; } = new(); + + public string Description { get; set; } = string.Empty; + + public Count Count { get; set; } = new(); + } + + public class Tag + { + public string Name { get; set; } = string.Empty; + + public bool IsNicodicExists { get; set; } + } + + public class Owner + { + public string Name { get; set; } = string.Empty; + + public string ID { get; set; } = string.Empty; + } + + public class Count + { + public int View { get; set; } + + public int Comment { get; set; } + + public int Mylist { get; set; } + + public int Like { get; set; } + } + + public class API + { + public string BaseUrl { get; set; } = string.Empty; + } +} diff --git a/Niconicome/Models/Domain/Local/Server/API/VideoInfo/V1/VideoInfoHandler.cs b/Niconicome/Models/Domain/Local/Server/API/VideoInfo/V1/VideoInfoHandler.cs new file mode 100644 index 00000000..8b7c5f48 --- /dev/null +++ b/Niconicome/Models/Domain/Local/Server/API/VideoInfo/V1/VideoInfoHandler.cs @@ -0,0 +1,203 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Text; +using System.Threading.Tasks; +using Niconicome.Models.Domain.Local.Server.API.VideoInfo.V1.Types; +using Niconicome.Models.Domain.Niconico.Net.Json; +using Niconicome.Models.Domain.Playlist; +using Niconicome.Models.Domain.Utils.Error; +using Niconicome.Models.Helper.Result; +using Niconicome.Models.Local.State; +using Niconicome.Models.Playlist.V2.Manager; +using NetConst = Niconicome.Models.Const.NetConstant; +using Err = Niconicome.Models.Domain.Local.Server.API.VideoInfo.V1.Error.VideoInfoHandlerError; +using System.Text.RegularExpressions; + +namespace Niconicome.Models.Domain.Local.Server.API.VideoInfo.V1 +{ + public interface IVideoInfoHandler + { + /// + /// 動画情報JSONを取得する + /// + /// + /// + /// + string GetVideoInfoJson(int port, string niconicoID, int playlistID); + + /// + /// リクエストを処理する + /// + /// + /// + /// + IAttemptResult Handle(int port, string url, HttpListenerResponse response); + } + + public class VideoInfoHandler(IPlaylistManager playlistManager, IErrorHandler errorHandler) : IVideoInfoHandler + { + #region field + + private readonly IPlaylistManager _playlistManager = playlistManager; + + private readonly IErrorHandler _errorHandler = errorHandler; + + #endregion + + public string GetVideoInfoJson(int port, string niconicoID, int playlistID) + { + IAttemptResult pResult = this._playlistManager.GetPlaylist(playlistID); + if (!pResult.IsSucceeded || pResult.Data is null) + { + var info = new JsWatchInfo(); + info.Meta.Status = (int)HttpStatusCode.NotFound; + info.Meta.Message = pResult.Message ?? string.Empty; + return JsonParser.Serialize(info); + } + + IVideoInfo? video = pResult.Data.Videos.FirstOrDefault(v => v.NiconicoId == niconicoID); + if (video is null) + { + var info = new JsWatchInfo(); + info.Meta.Status = (int)HttpStatusCode.NotFound; + info.Meta.Message = this._errorHandler.HandleError(Err.VideoNotFound, playlistID, niconicoID); + return JsonParser.Serialize(info); + } + + JsWatchInfo converted = this.Convert(port, video, pResult.Data); + return JsonParser.Serialize(converted); + } + + public IAttemptResult Handle(int port, string url, HttpListenerResponse response) + { + RequestType type = this.GetRequestType(url); + if (type == RequestType.None) + { + this.WriteMessage(this._errorHandler.HandleError(Err.InvalidRequest, url), (int)HttpStatusCode.BadRequest, response); + return AttemptResult.Succeeded(); + } + + var urlResult = this.GetPlaylistIDAndNiconicoID(url); + if (!urlResult.IsSucceeded) + { + this.WriteMessage(this._errorHandler.HandleError(Err.InvalidRequest, url), (int)HttpStatusCode.BadRequest, response); + return AttemptResult.Succeeded(); + } + + var (playlistID, niconicoID) = urlResult.Data; + string data = this.GetVideoInfoJson(port, niconicoID, playlistID); + + response.StatusCode = (int)HttpStatusCode.OK; + response.ContentType = "application/json"; + byte[] content = Encoding.UTF8.GetBytes(data); + response.OutputStream.Write(content, 0, content.Length); + return AttemptResult.Succeeded(); + } + + + + #region private + + private void WriteMessage(string message, int statusCode, HttpListenerResponse response) + { + var builder = new StringBuilder(); + builder.AppendLine(""); + builder.AppendLine(""); + builder.AppendLine(""); + builder.AppendLine($"Niconicome | {statusCode}"); + builder.AppendLine(""); + builder.AppendLine(""); + builder.AppendLine(""); + builder.AppendLine(message); + builder.AppendLine(""); + builder.AppendLine(""); + + byte[] content = Encoding.UTF8.GetBytes(builder.ToString()); + response.OutputStream.Write(content, 0, content.Length); + response.StatusCode = statusCode; + response.ContentType = "text/html"; + } + + private JsWatchInfo Convert(int port, IVideoInfo video, IPlaylistInfo playlist) + { + var info = new JsWatchInfo(); + info.Media.IsDownloaded = video.IsDownloaded.Value; + info.Meta.Status = (int)HttpStatusCode.OK; + info.Media.IsDMS = video.IsDMS; + + info.API.BaseUrl = $"http://localhost:{port}/niconicome/api/videoinfo/v1/{playlist.ID}/"; + + if (video.IsDMS && video.IsDownloaded.Value) + { + info.Media.ContentUrl = string.Format(NetConst.WatchAddressV1, port, playlist.ID, video.NiconicoId); + } + else + { + info.Media.ContentUrl = string.Format(NetConst.HLSAddressV1, port, playlist.ID, video.NiconicoId); + info.Media.CreateUrl = string.Format(NetConst.HLSCreateAddressV1, port, playlist.ID, video.NiconicoId); + } + + info.Comment.ContentUrl = string.Format(NetConst.CommentAddressV1, port, playlist.ID, video.NiconicoId); + info.Comment.CommentNGAPIBaseUrl = string.Format(NetConst.NGAPIAddressV1, port); + info.Thumbnail.ContentUrl = string.Format(NetConst.ThumbnailAddressV1, port, video.NiconicoId); + + info.Video.NiconicoID = video.NiconicoId; + info.Video.Title = video.Title; + info.Video.UploadedAt = video.UploadedOn; + info.Video.Tags = video.Tags.Select(tag => new Tag() { Name = tag.Name, IsNicodicExists = tag.IsNicodicExist }).ToList(); + info.Video.Owner.Name = video.OwnerName; + info.Video.Owner.ID = video.OwnerID; + info.Video.Description = video.Description; + info.Video.Duration = video.Duration; + info.Video.Count.View = video.ViewCount; + info.Video.Count.Comment = video.CommentCount; + info.Video.Count.Mylist = video.MylistCount; + info.Video.Count.Like = video.LikeCount; + + info.PlaylistVideos = playlist.Videos.Select(v => new PlaylistVideo() { NiconicoID = v.NiconicoId, Title = v.Title, UploadedAt = v.UploadedOn, ThumbnailURL = string.Format(NetConst.ThumbnailAddressV1, port, v.NiconicoId), Duration = v.Duration, ViewCount = v.ViewCount }).ToList(); + + return info; + } + + private IAttemptResult<(int, string)> GetPlaylistIDAndNiconicoID(string url) + { + string[] splitted = url.Split('/'); + string niconicoID = splitted[^1]; + string playlistID = splitted[^2]; + + if (!niconicoID.EndsWith(".json")) + { + return AttemptResult<(int, string)>.Fail(this._errorHandler.HandleError(Err.InvalidRequest, url)); + } + + niconicoID = niconicoID.Replace(".json", string.Empty); + + if (!int.TryParse(playlistID, out int playlistIDInt)) + { + return AttemptResult<(int, string)>.Fail(this._errorHandler.HandleError(Err.InvalidRequest, url)); + } + + return AttemptResult<(int, string)>.Succeeded((playlistIDInt, niconicoID)); + } + + private RequestType GetRequestType(string url) + { + if (Regex.IsMatch(url, @"https?://.+:\d+/niconicome/api/videoinfo/v1/\d+/.+\.json")) + { + return RequestType.VideoInfo; + } + + return RequestType.None; + } + + private enum RequestType + { + None, + VideoInfo, + } + + #endregion + } +} diff --git a/Niconicome/Models/Domain/Local/Server/API/Watch/V1/Error/DecryptorError.cs b/Niconicome/Models/Domain/Local/Server/API/Watch/V1/Error/DecryptorError.cs new file mode 100644 index 00000000..df92b1a2 --- /dev/null +++ b/Niconicome/Models/Domain/Local/Server/API/Watch/V1/Error/DecryptorError.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Niconicome.Models.Domain.Utils.Error; + +namespace Niconicome.Models.Domain.Local.Server.API.Watch.V1.Error +{ + public enum DecryptorError + { + [ErrorEnum(ErrorLevel.Error, "復号に失敗しました。")] + FailedToDecrypt, + } +} diff --git a/Niconicome/Models/Domain/Local/Server/API/Watch/V1/Error/PlaylistCreatorError.cs b/Niconicome/Models/Domain/Local/Server/API/Watch/V1/Error/PlaylistCreatorError.cs new file mode 100644 index 00000000..c1b79c79 --- /dev/null +++ b/Niconicome/Models/Domain/Local/Server/API/Watch/V1/Error/PlaylistCreatorError.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Niconicome.Models.Domain.Utils.Error; + +namespace Niconicome.Models.Domain.Local.Server.API.Watch.V1.Error +{ + public enum PlaylistCreatorError + { + [ErrorEnum(ErrorLevel.Error, "有効なストリームの検索に失敗しました。")] + StreamNotFound, + } +} diff --git a/Niconicome/Models/Domain/Local/Server/API/Watch/V1/Error/WatchHandlerError.cs b/Niconicome/Models/Domain/Local/Server/API/Watch/V1/Error/WatchHandlerError.cs new file mode 100644 index 00000000..3ae62055 --- /dev/null +++ b/Niconicome/Models/Domain/Local/Server/API/Watch/V1/Error/WatchHandlerError.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Niconicome.Models.Domain.Utils.Error; + +namespace Niconicome.Models.Domain.Local.Server.API.Watch.V1.Error +{ + public enum WatchHandlerError + { + [ErrorEnum(ErrorLevel.Error, "URLからセッションIDを取得できませんでした。(url:{0})")] + CannotExtractSessionID, + [ErrorEnum(ErrorLevel.Error, "URLからセッションプレイリスト・動画IDを取得できませんでした。(url:{0})")] + CannotExtractPlaylistAndVideoID, + [ErrorEnum(ErrorLevel.Error, "有効なストリームの検索に失敗しました。")] + StreamNotFound, + [ErrorEnum(ErrorLevel.Error,"ストリームの書き込みに失敗しました。")] + FailedToWriteStream, + } +} diff --git a/Niconicome/Models/Domain/Local/Server/API/Watch/V1/HLS/AES/AESInfomation.cs b/Niconicome/Models/Domain/Local/Server/API/Watch/V1/HLS/AES/AESInfomation.cs new file mode 100644 index 00000000..9dc09e83 --- /dev/null +++ b/Niconicome/Models/Domain/Local/Server/API/Watch/V1/HLS/AES/AESInfomation.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Niconicome.Models.Domain.Local.Server.API.Watch.V1.HLS.AES +{ + public interface IAESInfomation + { + /// + /// IV + /// + byte[] IV { get; } + + /// + /// Key + /// + byte[] Key { get; } + } + + public record AESInfomation(byte[] Key, byte[] IV) : IAESInfomation; +} diff --git a/Niconicome/Models/Domain/Local/Server/API/Watch/V1/HLS/AES/Decryptor.cs b/Niconicome/Models/Domain/Local/Server/API/Watch/V1/HLS/AES/Decryptor.cs new file mode 100644 index 00000000..4efca4ce --- /dev/null +++ b/Niconicome/Models/Domain/Local/Server/API/Watch/V1/HLS/AES/Decryptor.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Input; +using Niconicome.Models.Domain.Local.Server.API.Watch.V1.Error; +using Niconicome.Models.Domain.Utils.Error; +using Niconicome.Models.Helper.Result; + +namespace Niconicome.Models.Domain.Local.Server.API.Watch.V1.HLS.AES +{ + public interface IDecryptor + { + /// + /// 復号する + /// + /// + /// + /// + IAttemptResult Decrypt(byte[] cipher, IAESInfomation AESInfomation); + } + + public class Decryptor : IDecryptor + { + public Decryptor(IErrorHandler errorHandler) + { + this._errorHandler = errorHandler; + } + + #region field + + private readonly IErrorHandler _errorHandler; + + #endregion + + #region Method + + public IAttemptResult Decrypt(byte[] cipher, IAESInfomation AESInfomation) + { + try + { + using var aes = Aes.Create(); + + // HLS uses AES-128 w/ CBC & PKCS7 + // https://www.rfc-editor.org/rfc/rfc8216#section-4.3.2.4 + aes.BlockSize = 128; + aes.Padding = PaddingMode.PKCS7; + aes.Mode = CipherMode.CBC; + aes.KeySize = 128; + + aes.Key = AESInfomation.Key; + aes.IV = AESInfomation.IV; + + using var ms = new MemoryStream(cipher); + using var msPlain = new MemoryStream(); + using var cs = new CryptoStream(ms, aes.CreateDecryptor(), CryptoStreamMode.Read); + + cs.CopyTo(msPlain); + + return AttemptResult.Succeeded(msPlain.ToArray()); + } + catch (Exception ex) + { + this._errorHandler.HandleError(DecryptorError.FailedToDecrypt, ex); + return AttemptResult.Fail(this._errorHandler.GetMessageForResult(DecryptorError.FailedToDecrypt, ex)); + } + } + + #endregion + } +} diff --git a/Niconicome/Models/Domain/Local/Server/API/Watch/V1/HLS/PlaylistCreator.cs b/Niconicome/Models/Domain/Local/Server/API/Watch/V1/HLS/PlaylistCreator.cs new file mode 100644 index 00000000..f847755e --- /dev/null +++ b/Niconicome/Models/Domain/Local/Server/API/Watch/V1/HLS/PlaylistCreator.cs @@ -0,0 +1,106 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Niconicome.Models.Domain.Local.Server.API.Watch.V1.Error; +using Niconicome.Models.Domain.Local.Server.API.Watch.V1.LocalFile; +using Niconicome.Models.Domain.Local.Server.Core; +using Niconicome.Models.Domain.Playlist; +using Niconicome.Models.Domain.Utils.Error; +using Niconicome.Models.Helper.Result; +using Niconicome.Models.Playlist; + +namespace Niconicome.Models.Domain.Local.Server.API.Watch.V1.HLS +{ + public interface IPlaylistCreator + { + /// + /// プレイリストを生成する + /// + /// + /// + /// + /// + /// + HLSPlaylist GetPlaylist(IStreamInfo stream, string sessionID, int port); + } + + internal class PlaylistCreator : IPlaylistCreator + { + + public HLSPlaylist GetPlaylist(IStreamInfo stream, string sessionID, int port) + { + + string videoPlaylist = this.GetVideoPlaylist(stream, sessionID, port); + string audioPlaylist = this.GetAudioPlaylist(stream, sessionID, port); + string masterPlaylist = this.GetMasterPlaylist(stream, sessionID, port, stream.VideoBandWidth); + + return new HLSPlaylist(masterPlaylist, videoPlaylist, audioPlaylist, stream.VideoKey, stream.AudioKey); + + } + + private string GetVideoPlaylist(IStreamInfo stream, string sessionID, int port) + { + + var builder = new StringBuilder(); + builder.AppendLine("#EXTM3U"); + builder.AppendLine("#EXT-X-VERSION:6"); + builder.AppendLine("#EXT-X-TARGETDURATION:6"); + builder.AppendLine("#EXT-X-MEDIA-SEQUENCE:1"); + builder.AppendLine("#EXT-X-PLAYLIST-TYPE:VOD"); + builder.AppendLine($"#EXT-X-MAP:URI=\"http://localhost:{port}/niconicome/watch/v1/{sessionID}/video/init.cmfv\""); + builder.AppendLine($"#EXT-X-KEY:METHOD=AES-128,URI=\"http://localhost:{port}/niconicome/watch/v1/{sessionID}/video/key\",IV=\"{stream.VideoIV}\""); + foreach (var segment in stream.VideoSegments) + { + builder.AppendLine($"#EXTINF:{segment.Duration},"); + builder.AppendLine($"http://localhost:{port}/niconicome/watch/v1/{sessionID}/video/{segment.FileName}"); + } + builder.AppendLine("#EXT-X-ENDLIST"); + + return builder.ToString(); + } + + private string GetAudioPlaylist(IStreamInfo stream, string sessionID, int port) + { + + var builder = new StringBuilder(); + builder.AppendLine("#EXTM3U"); + builder.AppendLine("#EXT-X-VERSION:6"); + builder.AppendLine("#EXT-X-TARGETDURATION:6"); + builder.AppendLine("#EXT-X-MEDIA-SEQUENCE:1"); + builder.AppendLine("#EXT-X-PLAYLIST-TYPE:VOD"); + builder.AppendLine($"#EXT-X-MAP:URI=\"http://localhost:{port}/niconicome/watch/v1/{sessionID}/audio/init.cmfa\""); + builder.AppendLine($"#EXT-X-KEY:METHOD=AES-128,URI=\"http://localhost:{port}/niconicome/watch/v1/{sessionID}/audio/key\",IV=\"{stream.AudioIV}\""); + foreach (var segment in stream.AudioSegments) + { + builder.AppendLine($"#EXTINF:{segment.Duration},"); + builder.AppendLine($"http://localhost:{port}/niconicome/watch/v1/{sessionID}/audio/{segment.FileName}"); + } + builder.AppendLine("#EXT-X-ENDLIST"); + + return builder.ToString(); + } + + private string GetMasterPlaylist(IStreamInfo stream, string sessionID, int port, int bandWidth) + { + var builder = new StringBuilder(); + builder.AppendLine("#EXTM3U"); + builder.AppendLine("#EXT-X-VERSION:6"); + builder.AppendLine("#EXT-X-INDEPENDENT-SEGMENTS"); + builder.AppendLine($"#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"main-audio\",NAME=\"Main Audio\",DEFAULT=YES,URI=\"http://localhost:{port}/niconicome/watch/v1/{sessionID}/audio/playlist.m3u8\""); + builder.AppendLine($"#EXT-X-STREAM-INF:BANDWIDTH={bandWidth},AUDIO=\"main-audio\",RESOLUTION=\"{this.GetHorizontalResolution(stream.Resolution)}x{stream.Resolution}\""); + builder.AppendLine($"http://localhost:{port}/niconicome/watch/v1/{sessionID}/video/playlist.m3u8"); + + return builder.ToString(); + } + + private int GetHorizontalResolution(int verticalResolution) + { + return (int)Math.Round(verticalResolution * 16 / 9.0); + } + + } + + public record HLSPlaylist(string Master, string Video, string Audio, string videoKey, string audioKey); +} diff --git a/Niconicome/Models/Domain/Local/Server/API/Watch/V1/LocalFile/LocalFIleInfoHandler.cs b/Niconicome/Models/Domain/Local/Server/API/Watch/V1/LocalFile/LocalFIleInfoHandler.cs new file mode 100644 index 00000000..4acc0330 --- /dev/null +++ b/Niconicome/Models/Domain/Local/Server/API/Watch/V1/LocalFile/LocalFIleInfoHandler.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Niconicome.Models.Helper.Result; + +namespace Niconicome.Models.Domain.Local.Server.API.Watch.V1.LocalFile +{ + public interface ILocalFileInfoHandler + { + IAttemptResult GetLocalFileInfo(string filePath); + } +} diff --git a/Niconicome/Models/Domain/Local/Server/API/Watch/V1/LocalFile/LocalFileInfo.cs b/Niconicome/Models/Domain/Local/Server/API/Watch/V1/LocalFile/LocalFileInfo.cs new file mode 100644 index 00000000..25de630d --- /dev/null +++ b/Niconicome/Models/Domain/Local/Server/API/Watch/V1/LocalFile/LocalFileInfo.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Niconicome.Models.Domain.Local.Server.API.Watch.V1.LocalFile +{ + public interface ILocalFileInfo + { + Dictionary Streams { get; } + } + + public interface IStreamInfo + { + string VideoKey { get; } + + string AudioKey { get; } + + string VideoIV { get; } + + string AudioIV { get; } + + string VideoMapFileName { get; } + + string AudioMapFileName { get; } + + int VideoBandWidth { get; } + + int Resolution { get; } + + IEnumerable VideoSegments { get; } + + IEnumerable AudioSegments { get; } + } + + public interface ISegmentInfo + { + string FileName { get; } + + string Duration { get; } + } + +} diff --git a/Niconicome/Models/Domain/Local/Server/API/Watch/V1/Session/SessionManager.cs b/Niconicome/Models/Domain/Local/Server/API/Watch/V1/Session/SessionManager.cs new file mode 100644 index 00000000..4fff077e --- /dev/null +++ b/Niconicome/Models/Domain/Local/Server/API/Watch/V1/Session/SessionManager.cs @@ -0,0 +1,164 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Niconicome.Models.Domain.Local.Server.API.Watch.V1.LocalFile; +using Niconicome.Models.Domain.Niconico.Watch; + +namespace Niconicome.Models.Domain.Local.Server.API.Watch.V1.Session +{ + public interface ISessionManager + { + /// + /// セッションを作成する + /// + /// + string CreateSession(); + + /// + /// セッションを削除する + /// + /// + void DeleteSession(string sessionID); + + /// + /// セッションに情報を追加する + /// + /// + /// + /// + /// + /// + void EditSession(string sessionID, string folderPath, IEnumerable videoFileList, IEnumerable audioFileList, string videoKey, string audioKey, string videoMapFileName, string audioMapFileName, string videoPlaylist, string audioPlaylist, int resolution, string videoIV, string audioIV); + + /// + /// セッションの存在を確認 + /// + /// + /// + bool Exists(string sessionID); + + /// + /// セッションを取得する + /// + /// + /// + ISession GetSession(string sessionID); + + + } + + public interface ISession + { + string SessionID { get; } + + Dictionary Videos { get; } + + Dictionary Audios { get; } + + string VideoKey { get; } + + string AudioKey { get; } + + string VideoMapFileName { get; } + + string AudioMapFileName { get; } + + string AudioPlaylist { get; } + + string VideoPlaylist { get; } + + string FolderPath { get; } + + string VideoIV { get; } + + string AudioIV { get; } + + int Resolution { get; } + } + + public record Session : ISession + { + public required string SessionID { get; init; } + + public Dictionary Videos { get; init; } = new(); + + public Dictionary Audios { get; init; } = new(); + + public string VideoKey { get; set; } = string.Empty; + + public string AudioKey { get; set; } = string.Empty; + + public string VideoMapFileName { get; set; } = string.Empty; + + public string AudioMapFileName { get; set; } = string.Empty; + + public string AudioPlaylist { get; set; } = string.Empty; + + public string VideoPlaylist { get; set; } = string.Empty; + + public string FolderPath { get; set; } = string.Empty; + + public string VideoIV { get; set; } = string.Empty; + + public string AudioIV { get; set; } = string.Empty; + + public int Resolution { get; set; } + + } + + internal class SessionManager : ISessionManager + { + public string CreateSession() + { + string sessionID = Guid.NewGuid().ToString(); + this.sessions.Add(sessionID, new Session() { SessionID = sessionID }); + return sessionID; + } + + public void DeleteSession(string sessionID) + { + this.sessions.Remove(sessionID); + } + + public void EditSession(string sessionID, string folderPath, IEnumerable videoFileList, IEnumerable audioFileList, string videoKey, string audioKey, string videoMapFileName, string audioMapFileName, string videoPlaylist, string audioPlaylist, int resolution, string videoIV, string audioIV) + { + + var session = this.sessions[sessionID]; + session.FolderPath = folderPath; + session.VideoKey = videoKey; + session.AudioKey = audioKey; + session.AudioMapFileName = audioMapFileName; + session.VideoMapFileName = videoMapFileName; + session.AudioPlaylist = audioPlaylist; + session.VideoPlaylist = videoPlaylist; + session.Resolution = resolution; + session.VideoIV = videoIV; + session.AudioIV = audioIV; + + foreach (var v in videoFileList) + { + session.Videos.Add(v.FileName, v); + } + + foreach (var a in audioFileList) + { + session.Audios.Add(a.FileName, a); + } + } + + public bool Exists(string sessionID) + { + return this.sessions.ContainsKey(sessionID); + } + + public ISession GetSession(string sessionID) + { + return this.sessions[sessionID]; + } + + private readonly Dictionary sessions = new(); + } +} diff --git a/Niconicome/Models/Domain/Local/Server/API/Watch/V1/StringContent/WatchHandlerStringContent.cs b/Niconicome/Models/Domain/Local/Server/API/Watch/V1/StringContent/WatchHandlerStringContent.cs new file mode 100644 index 00000000..11954578 --- /dev/null +++ b/Niconicome/Models/Domain/Local/Server/API/Watch/V1/StringContent/WatchHandlerStringContent.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Niconicome.Models.Domain.Utils.StringHandler; + +namespace Niconicome.Models.Domain.Local.Server.API.Watch.V1.StringContent +{ + public enum WatchHandlerStringContent + { + [StringEnum("URLが不正です。")] + InvalidRequest, + [StringEnum("動画IDまたはプレイリストIDが不正です。")] + FailedToExtractVideoID, + [StringEnum("ストリームの読み込みに失敗しました 。")] + FailedToReadStream, + [StringEnum("ストリームの書き込みに失敗しました 。")] + FailedToWriteStream, + [StringEnum("指定された動画は存在しません。")] + NotFound, + [StringEnum("セッションIDが不正です。")] + SessionNotExist, + [StringEnum("ファイルの読み込みに失敗しました。")] + FailedtoLoadFile, + } +} diff --git a/Niconicome/Models/Domain/Local/Server/API/Watch/V1/WatchHandler.cs b/Niconicome/Models/Domain/Local/Server/API/Watch/V1/WatchHandler.cs new file mode 100644 index 00000000..963faf6b --- /dev/null +++ b/Niconicome/Models/Domain/Local/Server/API/Watch/V1/WatchHandler.cs @@ -0,0 +1,478 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Text; +using System.Text.RegularExpressions; +using Niconicome.Models.Domain.Local.IO.V2; +using Niconicome.Models.Domain.Local.Server.API.Watch.V1.HLS; +using Niconicome.Models.Domain.Local.Server.API.Watch.V1.HLS.AES; +using Niconicome.Models.Domain.Local.Server.API.Watch.V1.LocalFile; +using Niconicome.Models.Domain.Local.Server.API.Watch.V1.Session; +using Niconicome.Models.Domain.Playlist; +using Niconicome.Models.Domain.Utils.Error; +using Niconicome.Models.Domain.Utils.StringHandler; +using Niconicome.Models.Helper.Result; +using Niconicome.Models.Playlist.V2.Manager; +using Err = Niconicome.Models.Domain.Local.Server.API.Watch.V1.Error.WatchHandlerError; +using SC = Niconicome.Models.Domain.Local.Server.API.Watch.V1.StringContent.WatchHandlerStringContent; + +namespace Niconicome.Models.Domain.Local.Server.API.Watch.V1 +{ + public interface IWatchHandler + { + IAttemptResult Handle(HttpListenerResponse response, string url, int port); + } + + public class WatchHandler : IWatchHandler + { + public WatchHandler(ISessionManager sessionManager, IPlaylistCreator playlistCreator, ILocalFileInfoHandler localFileInfoHandler, IPlaylistManager playlistManager, IStringHandler stringHandler, IErrorHandler error, INiconicomeFileIO fileIO, IDecryptor decryptor) + { + this._sessionManager = sessionManager; + this._playlistCreator = playlistCreator; + this._localFileInfoHandler = localFileInfoHandler; + this._playlistManager = playlistManager; + this._stringHandler = stringHandler; + this._error = error; + this._fileIO = fileIO; + this._decryptor = decryptor; + } + + #region field + + private readonly ISessionManager _sessionManager; + + private readonly IPlaylistCreator _playlistCreator; + + private readonly ILocalFileInfoHandler _localFileInfoHandler; + + private readonly IPlaylistManager _playlistManager; + + private readonly IStringHandler _stringHandler; + + private readonly IErrorHandler _error; + + private readonly INiconicomeFileIO _fileIO; + + private readonly IDecryptor _decryptor; + + #endregion + + public IAttemptResult Handle(HttpListenerResponse response, string url, int port) + { + RequestType type = this.GetRequestType(url); + + //不明なリクエスト + if (type == RequestType.Invalid) + { + response.StatusCode = (int)HttpStatusCode.BadRequest; + response.ContentType = "text/html"; + this.WriteMessage(this._stringHandler.GetContent(SC.InvalidRequest), (int)HttpStatusCode.BadRequest, response.OutputStream); + return AttemptResult.Succeeded(); + } + else if (type == RequestType.MasterPlaylist) + { + this.HandleMaster(response, url, port); + return AttemptResult.Succeeded(); + } + + IAttemptResult sidResult = this.GetSessionID(url); + if (!sidResult.IsSucceeded || sidResult.Data is null) + { + response.ContentType = "text/html"; + response.StatusCode = (int)HttpStatusCode.BadRequest; + this.WriteMessage(this._stringHandler.GetContent(SC.InvalidRequest), (int)HttpStatusCode.BadRequest, response.OutputStream); + return AttemptResult.Succeeded(); + } + + if (!this._sessionManager.Exists(sidResult.Data)) + { + response.ContentType = "text/html"; + response.StatusCode = (int)HttpStatusCode.BadRequest; + this.WriteMessage(this._stringHandler.GetContent(SC.SessionNotExist), (int)HttpStatusCode.BadRequest, response.OutputStream); + return AttemptResult.Succeeded(); + } + + ISession session = this._sessionManager.GetSession(sidResult.Data); + + if (type == RequestType.VideoPlaylist || type == RequestType.AudioPlaylist) + { + this.HandlePlaylist(response, type, session); + return AttemptResult.Succeeded(); + } + else if (type == RequestType.VideoMap || type == RequestType.AudioMap) + { + this.HandleMap(response, type, session); + return AttemptResult.Succeeded(); + } + else if (type == RequestType.VideoKey || type == RequestType.AudioKey) + { + this.HandleKey(response, type, session); + return AttemptResult.Succeeded(); + } + else + { + this.HandleSegment(response, type, session, url); + return AttemptResult.Succeeded(); + } + + + } + + #region private + private void HandleMaster(HttpListenerResponse response, string url, int port) + { + string sid = this._sessionManager.CreateSession(); + + IAttemptResult<(int, string)> videoIDResult = this.GetPlaylistAndVideoID(url); + if (!videoIDResult.IsSucceeded) + { + response.StatusCode = (int)HttpStatusCode.BadRequest; + response.ContentType = "text/html"; + this.WriteMessage(this._stringHandler.GetContent(SC.FailedToExtractVideoID), (int)HttpStatusCode.BadRequest, response.OutputStream); + return; + } + + IAttemptResult playlistResult = this._playlistManager.GetPlaylist(videoIDResult.Data!.Item1); + if (!playlistResult.IsSucceeded || playlistResult.Data is null) + { + response.StatusCode = (int)HttpStatusCode.NotFound; + response.ContentType = "text/html"; + this.WriteMessage(this._stringHandler.GetContent(SC.NotFound), (int)HttpStatusCode.NotFound, response.OutputStream); + return; + } + + IVideoInfo? video = playlistResult.Data.Videos.FirstOrDefault(v => v.NiconicoId == videoIDResult.Data!.Item2); + if (video is null) + { + response.StatusCode = (int)HttpStatusCode.NotFound; + response.ContentType = "text/html"; + this.WriteMessage(this._stringHandler.GetContent(SC.NotFound), (int)HttpStatusCode.NotFound, response.OutputStream); + return; + } + + IAttemptResult fileResult = this._localFileInfoHandler.GetLocalFileInfo(video.FilePath); + if (!fileResult.IsSucceeded || fileResult.Data is null) + { + response.StatusCode = (int)HttpStatusCode.NotFound; + response.ContentType = "text/html"; + this.WriteMessage(this._stringHandler.GetContent(SC.NotFound), (int)HttpStatusCode.NotFound, response.OutputStream); + return; + } + + ILocalFileInfo file = fileResult.Data; + if (file.Streams.Count == 0) + { + this._error.HandleError(Err.StreamNotFound); + response.StatusCode = (int)HttpStatusCode.NotFound; + response.ContentType = "text/html"; + this.WriteMessage(this._stringHandler.GetContent(SC.NotFound), (int)HttpStatusCode.NotFound, response.OutputStream); + return; + } + + string folderPath = Path.GetDirectoryName(video.FilePath)!; + var (resolution, stream) = file.Streams.OrderByDescending(s => s.Key).First(); + HLSPlaylist playlist = this._playlistCreator.GetPlaylist(stream, sid, port); + + this._sessionManager.EditSession(sid, folderPath, stream.VideoSegments, stream.AudioSegments, stream.VideoKey, stream.AudioKey, stream.VideoMapFileName, stream.AudioMapFileName, playlist.Video, playlist.Audio, resolution, stream.VideoIV, stream.AudioIV); + try + { + response.ContentType = "application/vnd.apple.mpegurl"; + response.OutputStream.Write(Encoding.UTF8.GetBytes(playlist.Master)); + } + catch (Exception ex) + { + this._error.HandleError(Err.FailedToWriteStream, ex); + response.StatusCode = (int)HttpStatusCode.InternalServerError; + response.ContentType = "text/html"; + this.WriteMessage(this._stringHandler.GetContent(SC.FailedToWriteStream), (int)HttpStatusCode.InternalServerError, response.OutputStream); + return; + } + + response.StatusCode = (int)HttpStatusCode.OK; + } + + private void HandlePlaylist(HttpListenerResponse response, RequestType type, ISession session) + { + string playlistStr; + + if (type == RequestType.VideoPlaylist) + { + playlistStr = session.VideoPlaylist; + } + else + { + playlistStr = session.AudioPlaylist; + } + + try + { + response.ContentType = "application/vnd.apple.mpegurl"; + response.OutputStream.Write(Encoding.UTF8.GetBytes(playlistStr)); + } + catch (Exception ex) + { + this._error.HandleError(Err.FailedToWriteStream, ex); + response.StatusCode = (int)HttpStatusCode.InternalServerError; + response.ContentType = "text/html"; + this.WriteMessage(this._stringHandler.GetContent(SC.FailedToWriteStream), (int)HttpStatusCode.InternalServerError, response.OutputStream); + return; + } + + response.StatusCode = (int)HttpStatusCode.OK; + } + + private void HandleMap(HttpListenerResponse response, RequestType type, ISession session) + { + string mapFileName; + string contentType; + + if (type == RequestType.VideoMap) + { + mapFileName = Path.Combine(session.Resolution.ToString(), "video", session.VideoMapFileName); + contentType = "video/mp4"; + } + else + { + mapFileName = Path.Combine(session.Resolution.ToString(), "audio", session.AudioMapFileName); + contentType = "audio/mp4"; + } + + string path = Path.Combine(session.FolderPath, mapFileName); + + IAttemptResult readResult = this._fileIO.ReadByte(path); + if (!readResult.IsSucceeded || readResult.Data is null) + { + response.StatusCode = (int)HttpStatusCode.InternalServerError; + response.ContentType = "text/html"; + this.WriteMessage(this._stringHandler.GetContent(SC.FailedToReadStream), (int)HttpStatusCode.InternalServerError, response.OutputStream); + return; + } + + try + { + response.OutputStream.Write(readResult.Data); + } + catch (Exception ex) + { + this._error.HandleError(Err.FailedToWriteStream, ex); + response.StatusCode = (int)HttpStatusCode.InternalServerError; + response.ContentType = "text/html"; + this.WriteMessage(this._stringHandler.GetContent(SC.FailedToWriteStream), (int)HttpStatusCode.InternalServerError, response.OutputStream); + return; + } + + response.ContentType = contentType; + response.StatusCode = (int)HttpStatusCode.OK; + } + + private void HandleSegment(HttpListenerResponse response, RequestType type, ISession session, string url) + { + string fileName = Path.GetFileName(url); + string path; + string contentType; + string key; + string iv; + + if (type == RequestType.VideoSegment) + { + path = Path.Combine(session.FolderPath, session.Resolution.ToString(), "video", fileName); + contentType = "video/mp4"; + key = session.VideoKey; + iv = session.VideoIV; + } + else + { + path = Path.Combine(session.FolderPath, session.Resolution.ToString(), "audio", fileName); + contentType = "audio/mp4"; + key = session.AudioKey; + iv = session.AudioIV; + } + + IAttemptResult readResult = this._fileIO.ReadByte(path); + if (!readResult.IsSucceeded || readResult.Data is null) + { + response.StatusCode = (int)HttpStatusCode.InternalServerError; + response.ContentType = "text/html"; + this.WriteMessage(this._stringHandler.GetContent(SC.FailedToReadStream), (int)HttpStatusCode.InternalServerError, response.OutputStream); + return; + } + + //IAttemptResult decryptResult = this._decryptor.Decrypt(readResult.Data, new AESInfomation(Convert.FromBase64String(key), this.ToBytes(/iv))); + // + //if (!decryptResult.IsSucceeded || decryptResult.Data is null) + //{ + // response.StatusCode = (int)HttpStatusCode.InternalServerError; + // response.ContentType = "text/html"; + // response.OutputStream.Write(Encoding.UTF8.GetBytes(this._stringHandler.GetContent(SC.FailedToReadStream))); + // return; + //} + + try + { + response.OutputStream.Write(readResult.Data); + } + catch (Exception ex) + { + this._error.HandleError(Err.FailedToWriteStream, ex); + response.StatusCode = (int)HttpStatusCode.InternalServerError; + response.ContentType = "text/html"; + return; + } + + response.ContentType = contentType; + response.StatusCode = (int)HttpStatusCode.OK; + } + + private void HandleKey(HttpListenerResponse response, RequestType type, ISession session) + { + string key; + + if (type == RequestType.VideoKey) + { + key = session.VideoKey; + } + else + { + key = session.AudioKey; + } + + byte[] keyBytes = Convert.FromBase64String(key); + + try + { + response.OutputStream.Write(keyBytes); + } + catch (Exception ex) + { + this._error.HandleError(Err.FailedToWriteStream, ex); + response.StatusCode = (int)HttpStatusCode.InternalServerError; + response.ContentType = "text/html"; + this.WriteMessage(this._stringHandler.GetContent(SC.FailedToWriteStream), (int)HttpStatusCode.InternalServerError, response.OutputStream); + return; + } + + response.ContentType = "application/octet-stream"; + response.StatusCode = (int)HttpStatusCode.OK; + } + + private RequestType GetRequestType(string url) + { + + if (Regex.IsMatch(url, @"https?://.+:\d+/niconicome/watch/v1/\d+/.+/main\.m3u8")) + { + return RequestType.MasterPlaylist; + } + else if (Regex.IsMatch(url, @"https?://.+:\d+/niconicome/watch/v1/.+/audio\/playlist.m3u8")) + { + return RequestType.AudioPlaylist; + } + else if (Regex.IsMatch(url, @"https?://.+:\d+/niconicome/watch/v1/.+/video/playlist\.m3u8")) + { + return RequestType.VideoPlaylist; + } + else if (Regex.IsMatch(url, @"https?://.+:\d+/niconicome/watch/v1/.+/video/init\.cmfv")) + { + return RequestType.VideoMap; + } + else if (Regex.IsMatch(url, @"https?://.+:\d+/niconicome/watch/v1/.+/audio/init\.cmfa")) + { + return RequestType.AudioMap; + } + else if (Regex.IsMatch(url, @"https?://.+:\d+/niconicome/watch/v1/.+/video/.+\.cmfv")) + { + return RequestType.VideoSegment; + } + else if (Regex.IsMatch(url, @"https?://.+:\d+/niconicome/watch/v1/.+/audio/.+\.cmfa")) + { + return RequestType.AudioSegment; + } + else if (Regex.IsMatch(url, @"https?://.+:\d+/niconicome/watch/v1/.+/audio/key")) + { + return RequestType.AudioKey; + } + else if (Regex.IsMatch(url, @"https?://.+:\d+/niconicome/watch/v1/.+/video/key")) + { + return RequestType.VideoKey; + } + else + { + return RequestType.Invalid; + } + } + + private IAttemptResult GetSessionID(string rawURL) + { + var url = new Uri(rawURL); + string[] splited = url.AbsolutePath.TrimStart('/').Split("/"); + + if (splited.Length < 4) + { + return AttemptResult.Fail(this._error.HandleError(Err.CannotExtractSessionID, rawURL)); + } + + return AttemptResult.Succeeded(splited[3]); + } + + private IAttemptResult<(int, string)> GetPlaylistAndVideoID(string rawURL) + { + var url = new Uri(rawURL); + string[] splited = url.AbsolutePath.TrimStart('/').Split("/"); + + if (splited.Length < 5) + { + return AttemptResult<(int, string)>.Fail(this._error.HandleError(Err.CannotExtractPlaylistAndVideoID, rawURL)); + } + + return AttemptResult<(int, string)>.Succeeded((int.Parse(splited[3]), splited[4])); + } + + private byte[] ToBytes(string str) + { + var bs = new List(); + + for (var i = 2; i < str.Length - 1; i += 2) + { + bs.Add(Convert.ToByte(str.Substring(i, 2), 16)); + } + + return bs.ToArray(); + } + + private void WriteMessage(string message, int status, Stream output) + { + var builder = new StringBuilder(); + builder.AppendLine(""); + builder.AppendLine(""); + builder.AppendLine(""); + builder.AppendLine($"Niconicome | {status}"); + builder.AppendLine(""); + builder.AppendLine(""); + builder.AppendLine(""); + builder.AppendLine(message); + builder.AppendLine(""); + builder.AppendLine(""); + + byte[] content = Encoding.UTF8.GetBytes(builder.ToString()); + output.Write(content, 0, content.Length); + } + + #endregion + + private enum RequestType + { + Invalid, + MasterPlaylist, + AudioPlaylist, + VideoPlaylist, + VideoSegment, + AudioSegment, + VideoMap, + AudioMap, + VideoKey, + AudioKey, + } + + } +} diff --git a/Niconicome/Models/Domain/Local/Server/Core/IPHandler.cs b/Niconicome/Models/Domain/Local/Server/Core/IPHandler.cs new file mode 100644 index 00000000..afa6a9ba --- /dev/null +++ b/Niconicome/Models/Domain/Local/Server/Core/IPHandler.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Text; +using System.Threading.Tasks; + +namespace Niconicome.Models.Domain.Local.Server.Core +{ + public interface IIPHandler + { + /// + /// ローカルIPアドレスを取得する + /// + /// + string GetMyLocalIP(); + } + + public class IPHandler : IIPHandler + { + private string? _localIP; + + public string GetMyLocalIP() + { + if (this._localIP is not null) return _localIP; + + string hostname = Dns.GetHostName(); + IPAddress[] adrList = Dns.GetHostAddresses(hostname); + + foreach (var address in adrList) + { + if (address.AddressFamily == AddressFamily.InterNetwork) + { + this._localIP = address.ToString(); + return this._localIP; + } + } + + return "localhost"; + } + + } +} diff --git a/Niconicome/Models/Domain/Local/Server/Core/RequestType.cs b/Niconicome/Models/Domain/Local/Server/Core/RequestType.cs index 9a65633b..94041987 100644 --- a/Niconicome/Models/Domain/Local/Server/Core/RequestType.cs +++ b/Niconicome/Models/Domain/Local/Server/Core/RequestType.cs @@ -13,5 +13,11 @@ public enum RequestType M3U8, TS, UserChrome, + WatchAPI, + CommentAPI, + RegacyHLSAPI, + ResourceAPI, + VideoInfoAPI, + NG, } } diff --git a/Niconicome/Models/Domain/Local/Server/Core/Server.cs b/Niconicome/Models/Domain/Local/Server/Core/Server.cs index 01719d13..e5934428 100644 --- a/Niconicome/Models/Domain/Local/Server/Core/Server.cs +++ b/Niconicome/Models/Domain/Local/Server/Core/Server.cs @@ -4,6 +4,12 @@ using System.Net; using System.Threading.Tasks; using Microsoft.WindowsAPICodePack.Taskbar; +using Niconicome.Models.Domain.Local.Server.API.Comment.V1; +using Niconicome.Models.Domain.Local.Server.API.NG.V1; +using Niconicome.Models.Domain.Local.Server.API.RegacyHLS.V1; +using Niconicome.Models.Domain.Local.Server.API.Resource.V1; +using Niconicome.Models.Domain.Local.Server.API.VideoInfo.V1; +using Niconicome.Models.Domain.Local.Server.API.Watch.V1; using Niconicome.Models.Domain.Local.Server.RequestHandler.M3U8; using Niconicome.Models.Domain.Local.Server.RequestHandler.NotFound; using Niconicome.Models.Domain.Local.Server.RequestHandler.TS; @@ -36,16 +42,23 @@ public interface IServer public class Server : IServer { - public Server(IUrlHandler urlHandler, IVideoRequestHandler video, INotFoundRequestHandler notFound, IErrorHandler errorHandler, IM3U8RequestHandler m3U8, ITSRequestHandler ts, IUserChromeRequestHandler userChrome, IPortHandler portHandler) + public Server(IUrlHandler urlHandler, IVideoRequestHandler video, INotFoundRequestHandler notFound, IM3U8RequestHandler m3U8, ITSRequestHandler ts, IUserChromeRequestHandler userChrome, IErrorHandler errorHandler, IPortHandler portHandler, IWatchHandler watchHandler, ICommentRequestHandler commentRequestHandler, IRegacyHLSHandler regacyHLSHandler, IIPHandler iPHandler, IResourceHandler resourceHandler, IVideoInfoHandler videoInfoHandler,INGHandler nGHandler) { this._urlHandler = urlHandler; this._video = video; this._notFound = notFound; - this._errorHandler = errorHandler; this._m3U8 = m3U8; this._ts = ts; this._userChrome = userChrome; + this._errorHandler = errorHandler; this._portHandler = portHandler; + this._watchHandler = watchHandler; + this._commentRequestHandler = commentRequestHandler; + this._regacyHLSHandler = regacyHLSHandler; + this._resourceHandler = resourceHandler; + this._videoInfoHandler = videoInfoHandler; + this._nGHandler = nGHandler; + this._iPHandler = iPHandler; } ~Server() @@ -71,6 +84,20 @@ public Server(IUrlHandler urlHandler, IVideoRequestHandler video, INotFoundReque private readonly IPortHandler _portHandler; + private readonly IWatchHandler _watchHandler; + + private readonly ICommentRequestHandler _commentRequestHandler; + + private readonly IRegacyHLSHandler _regacyHLSHandler; + + private readonly IResourceHandler _resourceHandler; + + private readonly IVideoInfoHandler _videoInfoHandler; + + private readonly INGHandler _nGHandler; + + private readonly IIPHandler _iPHandler; + private readonly Queue _ports = new(); private bool _isRunning; @@ -89,7 +116,7 @@ public Server(IUrlHandler urlHandler, IVideoRequestHandler video, INotFoundReque public void Start() { - if (this._isRunning||this._isShutdowned) + if (this._isRunning || this._isShutdowned) { return; } @@ -124,6 +151,7 @@ public void Start() listnner.Prefixes.Clear(); listnner.Prefixes.Add($"http://localhost:{this.Port}/"); + listnner.Prefixes.Add($"http://127.0.0.1:{this.Port}/"); listnner.Start(); this._errorHandler.HandleError(ServerError.ServerStarted, this.Port); @@ -132,81 +160,143 @@ public void Start() { HttpListenerContext context = listnner.GetContext(); - HttpListenerRequest request = context.Request; - HttpListenerResponse response = context.Response; + _ = Task.Run(async () => + { + HttpListenerRequest request = context.Request; + HttpListenerResponse response = context.Response; - //CORS - response.Headers.Add("Access-Control-Allow-Origin", "*"); + this._errorHandler.HandleError(ServerError.RequestHandled, request.Url!.ToString(), request.UserAgent); - if (request.Url is null) - { - context.Response.Close(); - continue; - } + //CORS + response.Headers.Add("Access-Control-Allow-Origin", "*"); - if (request.HttpMethod == "OPTIONS") - { - response.Headers.Add("Access-Control-Allow-Methods", "GET, OPTIONS"); - response.StatusCode = (int)HttpStatusCode.OK; - response.Close(); - continue; - } + if (request.Url is null) + { + context.Response.Close(); + return; + } - if (request.HttpMethod != "GET") - { - context.Response.Close(); - continue; - } + if (request.HttpMethod == "OPTIONS") + { + response.Headers.Add("Access-Control-Allow-Methods", "GET, OPTIONS"); + response.StatusCode = (int)HttpStatusCode.OK; + response.Close(); + return; + } - RequestType type = this._urlHandler.GetReqyestType(request.Url); - IAttemptResult? result = null; + if (request.HttpMethod != "GET" && request.HttpMethod != "POST") + { + context.Response.Close(); + return; + } - if (type == RequestType.Video) - { - try + RequestType type = this._urlHandler.GetReqyestType(request.Url); + IAttemptResult? result = null; + + if (type == RequestType.Video) { - result = this._video.Handle(request.Url, response); + try + { + result = this._video.Handle(request.Url, response); + } + catch { } } - catch { } - } - if (type == RequestType.M3U8) - { - try + if (type == RequestType.M3U8) { - result = this._m3U8.Handle(response); + try + { + result = this._m3U8.Handle(response); + } + catch { } } - catch { } - } - if (type == RequestType.TS) - { - try + if (type == RequestType.TS) { - result = this._ts.Handle(request.Url, response); + try + { + result = this._ts.Handle(request.Url, response); + } + catch { } } - catch { } - } - if (type == RequestType.UserChrome) - { - try + if (type == RequestType.UserChrome) { - result = this._userChrome.Handle(response); + try + { + result = this._userChrome.Handle(response); + } + catch { } } - catch { } - } - if (result is null || !result.IsSucceeded) - { - try + if (type == RequestType.WatchAPI) { - this._notFound.Handle(request.Url, response, result?.Message); + try + { + result = this._watchHandler.Handle(response, request.Url.ToString(), this.Port); + } + catch { } } - catch { } - } - response.Close(); + if (type == RequestType.CommentAPI) + { + try + { + result = this._commentRequestHandler.Handle(request.Url.ToString(), response); + } + catch { } + } + + if (type == RequestType.RegacyHLSAPI) + { + try + { + result = await this._regacyHLSHandler.Handle(request.Url.ToString(), response, this.Port); + } + catch { } + } + + if (type == RequestType.ResourceAPI) + { + try + { + result = this._resourceHandler.Handle(request.Url.ToString(), response); + } + catch { } + } + + if (type == RequestType.VideoInfoAPI) + { + try + { + result = this._videoInfoHandler.Handle(this.Port, request.Url.ToString(), response); + } + catch { } + } + + if (type == RequestType.NG) + { + try + { + result = this._nGHandler.Handle(request, response); + } + catch { } + } + + if (result is null || !result.IsSucceeded) + { + try + { + this._notFound.Handle(request.Url, response, result?.Message); + } + catch { } + } + + + response.Close(); + }); + + } listnner.Close(); diff --git a/Niconicome/Models/Domain/Local/Server/Core/ServerError.cs b/Niconicome/Models/Domain/Local/Server/Core/ServerError.cs index 9c554174..7d0b0aa0 100644 --- a/Niconicome/Models/Domain/Local/Server/Core/ServerError.cs +++ b/Niconicome/Models/Domain/Local/Server/Core/ServerError.cs @@ -15,5 +15,7 @@ public enum ServerError ServerStoppedWithException, [ErrorEnum(ErrorLevel.Log, "ローカルサーバーをシャットダウンしました。")] ServerStopped, + [ErrorEnum(ErrorLevel.Log, "リクエストを処理しました。(url:{0} ua:{1})")] + RequestHandled, } } diff --git a/Niconicome/Models/Domain/Local/Server/Core/UrlHandler.cs b/Niconicome/Models/Domain/Local/Server/Core/UrlHandler.cs index fbd33789..91646919 100644 --- a/Niconicome/Models/Domain/Local/Server/Core/UrlHandler.cs +++ b/Niconicome/Models/Domain/Local/Server/Core/UrlHandler.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Configuration; using System.Linq; using System.Text; using System.Text.RegularExpressions; @@ -43,6 +44,36 @@ public RequestType GetReqyestType(Uri request) return RequestType.UserChrome; } + if (Regex.IsMatch(path, @"/niconicome/watch/v1/.+")) + { + return RequestType.WatchAPI; + } + + if (Regex.IsMatch(path, @"/niconicome/api/comment/v1/.+")) + { + return RequestType.CommentAPI; + } + + if (Regex.IsMatch(path, @"/niconicome/api/regacyhls/v1/.+")) + { + return RequestType.RegacyHLSAPI; + } + + if (Regex.IsMatch(path, @"/niconicome/resource/v1/.+") || path == "/favicon.ico") + { + return RequestType.ResourceAPI; + } + + if (Regex.IsMatch(path, @"/niconicome/api/videoinfo/v1/.+")) + { + return RequestType.VideoInfoAPI; + } + + if (Regex.IsMatch(path, @"/niconicome/api/ng/v1/.+")) + { + return RequestType.NG; + } + return RequestType.None; } } diff --git a/Niconicome/Models/Domain/Local/Settings/SettingInfo.cs b/Niconicome/Models/Domain/Local/Settings/SettingInfo.cs index 8f00ac64..97ebc5ad 100644 --- a/Niconicome/Models/Domain/Local/Settings/SettingInfo.cs +++ b/Niconicome/Models/Domain/Local/Settings/SettingInfo.cs @@ -6,11 +6,12 @@ using Niconicome.Models.Domain.Local.Store.V2; using Niconicome.Models.Domain.Playlist; using Niconicome.Models.Helper.Result; +using Niconicome.Models.Utils.Reactive; using Reactive.Bindings; namespace Niconicome.Models.Domain.Local.Settings { - public interface ISettingInfo where T : notnull + public interface ISettingInfo where T : notnull { /// /// 設定名 @@ -27,6 +28,11 @@ public interface ISettingInfo where T : notnull /// ReactiveProperty ReactiveValue { get; } + /// + /// 設定値(BP) + /// + IBindableProperty Bindablevalue { get; } + /// /// 変更監視 /// @@ -73,6 +79,16 @@ public SettingInfo(string settingName, T initialValue, ISettingsStore store) this._store.SetSetting(this); }); + this.Bindablevalue = new BindableProperty(initialValue); + this.Bindablevalue.RegisterPropertyChangeHandler(value => + { + if (EqualityComparer.Default.Equals(value, this._value)) return; + + this._value = value; + this.OnChange(value); + this._store.SetSetting(this); + }); + } #region field @@ -108,12 +124,15 @@ public T Value this._value = value; this.OnChange(value); this.ReactiveValue.Value = value; + this.Bindablevalue.Value = value; this._store.SetSetting(this); } } public ReactiveProperty ReactiveValue { get; init; } + public IBindableProperty Bindablevalue { get; init; } + #endregion diff --git a/Niconicome/Models/Domain/Local/Settings/SettingsNames.cs b/Niconicome/Models/Domain/Local/Settings/SettingsNames.cs index 6a1632a1..ddd64728 100644 --- a/Niconicome/Models/Domain/Local/Settings/SettingsNames.cs +++ b/Niconicome/Models/Domain/Local/Settings/SettingsNames.cs @@ -50,7 +50,7 @@ public static class SettingNames public static string PostDownloadAction { get; private set; } = "Download.General.PostDownloadAction"; //Download.General.DownloadCompletionAudio - public static string PlaySoundAfterDownload { get; private set; }= "Download.General.DownloadCompletionAudio.Enable"; + public static string PlaySoundAfterDownload { get; private set; } = "Download.General.DownloadCompletionAudio.Enable"; public static string DownloadCompletionAudioPath { get; private set; } = "Download.General.DownloadCompletionAudio.Path"; @@ -78,6 +78,13 @@ public static class SettingNames public static string ExternalDLSoftwareParam { get; private set; } = "Download.Video.ExternalSoftware.Parameter"; + //Download.Video.Modification + public static string IsVideoModificationEnable { get; private set; } = "Download.Video.Modification.IsVideoModificationEnable"; + + public static string VideoModificationSoftwarePath { get; private set; } = "Download.Video.Modification.VideoModificationSoftwarePath"; + + public static string VideoModificationSoftwareParam { get; private set; } = "Download.Video.Modification.VideoModificationParameter"; + //Download.Comment public static string CommentOffset { get; private set; } = "Download.Comment.Commentoffset"; @@ -232,6 +239,13 @@ public static class SettingNames //Videolist.File public static string SearchVideosExact { get; private set; } = "Videolist.File.SearchVideosExact"; + //Watch.NG + public static string NGUsers { get; private set; } = "Watch.NG.NGUsers"; + + public static string NGWords { get; private set; } = "Watch.NG.NGWords"; + + public static string NGCommands { get; private set; } = "Watch.NG.NGCommands"; + //Application public static string LimitWindowsToSingleton { get; private set; } = "Application.LimitWindowsToSingleton"; diff --git a/Niconicome/Models/Domain/Local/Store/V2/CookieStore.cs b/Niconicome/Models/Domain/Local/Store/V2/CookieStore.cs new file mode 100644 index 00000000..32fb05ce --- /dev/null +++ b/Niconicome/Models/Domain/Local/Store/V2/CookieStore.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Niconicome.Models.Domain.Niconico.UserAuth; +using Niconicome.Models.Helper.Result; + +namespace Niconicome.Models.Domain.Local.Store.V2 +{ + public interface ICookieStore + { + /// + /// Cookieを取得 + /// + /// + IAttemptResult GetCookieInfo(); + + /// + /// Cookieを削除 + /// + /// + IAttemptResult DeleteCookieInfo(); + + /// + /// Cookie情報を上書き + /// + /// + /// + IAttemptResult Update(ICookieInfo cookie); + + /// + /// Cookieが存在するかどうか + /// + /// + bool Exists(); + } +} diff --git a/Niconicome/Models/Domain/Network/NetWorkState.cs b/Niconicome/Models/Domain/Network/NetWorkState.cs new file mode 100644 index 00000000..1bfa2aaa --- /dev/null +++ b/Niconicome/Models/Domain/Network/NetWorkState.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Text; +using System.Threading.Tasks; +using Niconicome.Models.Domain.Niconico; +using Niconicome.Models.Utils.Reactive; + +namespace Niconicome.Models.Domain.Network +{ + public interface INetWorkState + { + Task IsNetWorkAvailable(); + } + + public class NetWorkState : INetWorkState + { + public NetWorkState(INicoHttp http) + { + this._http = http; + } + + private readonly INicoHttp _http; + + public async Task IsNetWorkAvailable() + { + try + { + var result = await this._http.GetAsync(new Uri("http://clients3.google.com/generate_204")); + return result.StatusCode == HttpStatusCode.NoContent; + } + catch + { + return false; + } + } + } +} diff --git a/Niconicome/Models/Domain/Niconico/Download/General/LocalFileHandler.cs b/Niconicome/Models/Domain/Niconico/Download/General/LocalFileHandler.cs index 34ecc638..eb09ff00 100644 --- a/Niconicome/Models/Domain/Niconico/Download/General/LocalFileHandler.cs +++ b/Niconicome/Models/Domain/Niconico/Download/General/LocalFileHandler.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.IO; using System.Linq; using Niconicome.Models.Const; using Niconicome.Models.Domain.Local.IO.V2; @@ -62,9 +63,16 @@ public IAttemptResult GetLocalContentInfo(string folderPath, str return AttemptResult.Fail(filesResult.Message); } + IAttemptResult> dirResult = this._directoryIO.GetDirectories(folderPath); + if (!dirResult.IsSucceeded || dirResult.Data is null) + { + return AttemptResult.Fail(dirResult.Message); + } + IEnumerable files = filesResult.Data.Where(f=>f.Contains(niconicoID)); + IEnumerable dirs = dirResult.Data.Where(d=>d.Contains(niconicoID)); - bool videoExist = files.Any(f => this.GetFileType(f, thumbnailExt, ichibaSuffix, videoInfosuffix) == FileType.Video); + bool videoExist = dirs.Any(f => this.IsVideoExists(f,verticalResolution)); bool commentExist = files.Any(f => this.GetFileType(f, thumbnailExt, ichibaSuffix, videoInfosuffix) == FileType.Comment); bool ichibaInfoExist = files.Any(f => this.GetFileType(f, thumbnailExt, ichibaSuffix, videoInfosuffix) == FileType.Ichiba); bool videoInfoExist = files.Any(f => this.GetFileType(f, thumbnailExt, ichibaSuffix, videoInfosuffix) == FileType.VideoInfo); @@ -142,6 +150,17 @@ private FileType GetFileType(string fileName, string thumbnailExt, string ichiba } } + /// + /// 動画が存在するかどうか(DMS) + /// + /// + /// + /// + private bool IsVideoExists(string folderPath,uint verticalResolution) + { + return this._directoryIO.Exists(Path.Combine(folderPath, verticalResolution.ToString())); + } + private enum FileType { Unknown, diff --git a/Niconicome/Models/Domain/Niconico/Download/Video/V2/Error/WatchSessionError.cs b/Niconicome/Models/Domain/Niconico/Download/Video/V2/Error/WatchSessionError.cs index 88106a54..36a79de3 100644 --- a/Niconicome/Models/Domain/Niconico/Download/Video/V2/Error/WatchSessionError.cs +++ b/Niconicome/Models/Domain/Niconico/Download/Video/V2/Error/WatchSessionError.cs @@ -27,5 +27,7 @@ public enum WatchSessionError SucceededToSendHeartBeat, [ErrorEnum(ErrorLevel.Error,"アドオンが不正な情報を返却しました。(id:{0})")] AddonReturnedInvalidInfomation, + [ErrorEnum(ErrorLevel.Error, "セッション確立アドオンが登録されていません。")] + AddonNotRegistered, } } diff --git a/Niconicome/Models/Domain/Niconico/Download/Video/V2/Integrate/VideoDownloader.cs b/Niconicome/Models/Domain/Niconico/Download/Video/V2/Integrate/VideoDownloader.cs index ff432081..666be2e6 100644 --- a/Niconicome/Models/Domain/Niconico/Download/Video/V2/Integrate/VideoDownloader.cs +++ b/Niconicome/Models/Domain/Niconico/Download/Video/V2/Integrate/VideoDownloader.cs @@ -4,12 +4,10 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; -using ABI.System; -using System.Windows.Input; using Niconicome.Models.Const; -using Niconicome.Models.Domain.Local.External.Software.NiconicomeProcess; using Niconicome.Models.Domain.Local.IO.V2; using Niconicome.Models.Domain.Local.Store.V2; +using Niconicome.Models.Domain.Niconico.Download.Video.V2.External; using Niconicome.Models.Domain.Niconico.Download.Video.V2.Fetch.Segment; using Niconicome.Models.Domain.Niconico.Download.Video.V2.Fetch.Segment.AES; using Niconicome.Models.Domain.Niconico.Download.Video.V2.HLS.Stream; @@ -23,7 +21,6 @@ using Niconicome.Models.Network.Download; using Niconicome.Models.Utils.ParallelTaskV2; using SC = Niconicome.Models.Domain.Niconico.Download.Video.V2.Integrate.VideoDownloaderSC; -using Niconicome.Models.Domain.Niconico.Download.Video.V2.External; namespace Niconicome.Models.Domain.Niconico.Download.Video.V2.Integrate { diff --git a/Niconicome/Models/Domain/Niconico/Download/Video/V2/Session/WatchSession.cs b/Niconicome/Models/Domain/Niconico/Download/Video/V2/Session/WatchSession.cs index 1eb612fa..c5cb6679 100644 --- a/Niconicome/Models/Domain/Niconico/Download/Video/V2/Session/WatchSession.cs +++ b/Niconicome/Models/Domain/Niconico/Download/Video/V2/Session/WatchSession.cs @@ -45,11 +45,10 @@ public interface IWatchSession : IDisposable /// public class WatchSession : IWatchSession { - public WatchSession(IWatchInfohandler watchInfo, INicoHttp http, Utils::ILogger logger, IDmcDataHandler dmchandler, IMasterPlaylisthandler playlisthandler, IHooksManager hooksManager, IErrorHandler errorHandler) + public WatchSession(IWatchInfohandler watchInfo, INicoHttp http, Utils::ILogger logge, IMasterPlaylisthandler playlisthandler, IHooksManager hooksManager, IErrorHandler errorHandler) { this._watchInfo = watchInfo; this._http = http; - this._dmchandler = dmchandler; this._playlisthandler = playlisthandler; this._hooksManager = hooksManager; this._errorHandler = errorHandler; @@ -64,8 +63,6 @@ public WatchSession(IWatchInfohandler watchInfo, INicoHttp http, Utils::ILogger private readonly IErrorHandler _errorHandler; - private readonly IDmcDataHandler _dmchandler; - private readonly IWatchInfohandler _watchInfo; private readonly IMasterPlaylisthandler _playlisthandler; @@ -106,17 +103,13 @@ public async Task EnsureSessionAsync(IDomainVideoInfo videoInfo) } //セッション確立 - IAttemptResult result; - - if (this._hooksManager.IsRegistered(HookType.SessionEnsuring)) - { - result = await this.EnsureSessionWithAddonAsync(videoInfo); - } - else + if (!this._hooksManager.IsRegistered(HookType.SessionEnsuring)) { - result = await this.EnsureSessionDefaultAsync(videoInfo); + return AttemptResult.Fail(this._errorHandler.HandleError(Err.AddonNotRegistered)); } + IAttemptResult result = await this.EnsureSessionWithAddonAsync(videoInfo); + if (!result.IsSucceeded || result.Data is null) { return AttemptResult.Fail(result.Message); @@ -167,29 +160,6 @@ public void Dispose() #region private - /// - /// セッションを確立する - /// - /// - /// - private async Task> EnsureSessionDefaultAsync(IDomainVideoInfo video) - { - IWatchSessionInfo sessionInfo; - - try - { - sessionInfo = await this._dmchandler.GetSessionInfoAsync(video); - } - catch (Exception ex) - { - this._errorHandler.HandleError(Err.SessionEnsuringFailure, ex, video.Id); - return AttemptResult.Fail(this._errorHandler.GetMessageForResult(Err.SessionEnsuringFailure, ex, video.Id)); - } - - return AttemptResult.Succeeded(sessionInfo); - - } - /// /// アドオンでセッションを確立する /// @@ -214,7 +184,7 @@ private async Task> EnsureSessionWithAddonAsyn try { - if (result.Data.DmcResponseJsonData is not string jsonData || result.Data.ContentUrl is not string contentUrl || result.Data.SessionId is not string sessionID) + if (result.Data.DmcResponseJsonData is not string jsonData || result.Data.ContentUrl is not string contentUrl || result.Data.SessionId is not string sessionID || result.Data.IsDMS is not bool isDMS) { this._errorHandler.HandleError(Err.AddonReturnedInvalidInfomation, video.Id); return AttemptResult.Fail(this._errorHandler.GetMessageForResult(Err.AddonReturnedInvalidInfomation, video.Id)); @@ -225,6 +195,7 @@ private async Task> EnsureSessionWithAddonAsyn DmcResponseJsonData = jsonData, ContentUrl = contentUrl, SessionId = sessionID, + IsDMS = isDMS, }; return new AttemptResult() { IsSucceeded = true, Data = info }; diff --git a/Niconicome/Models/Domain/Niconico/Download/Video/V3/DMS/HLS/M3U8Node.cs b/Niconicome/Models/Domain/Niconico/Download/Video/V3/DMS/HLS/M3U8Node.cs new file mode 100644 index 00000000..c9d538a0 --- /dev/null +++ b/Niconicome/Models/Domain/Niconico/Download/Video/V3/DMS/HLS/M3U8Node.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Niconicome.Models.Domain.Niconico.Download.Video.V3.DMS.HLS +{ + public interface IM3U8Node + { + /// + /// ノードの種類 + /// + M3U8NodeType Type { get; } + + /// + /// 値 + /// + string Value { get; } + + /// + /// URL + /// + string URL { get; } + } + + //IM3U8Nodeを実装する + public class M3U8Node : IM3U8Node + { + public M3U8Node(M3U8NodeType type, string value, string url) + { + this.Type = type; + this.Value = value; + this.URL = url; + } + + public M3U8NodeType Type { get; set; } + + public string Value { get; set; } = string.Empty; + + public string URL { get; set; } = string.Empty; + } + + public enum M3U8NodeType + { + StreamInfo, + Audio, + Key, + Segment, + InitializationSection, + } +} diff --git a/Niconicome/Models/Domain/Niconico/Download/Video/V3/DMS/HLS/M3U8Parser.cs b/Niconicome/Models/Domain/Niconico/Download/Video/V3/DMS/HLS/M3U8Parser.cs new file mode 100644 index 00000000..20da9f26 --- /dev/null +++ b/Niconicome/Models/Domain/Niconico/Download/Video/V3/DMS/HLS/M3U8Parser.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Niconicome.Models.Domain.Niconico.Download.Video.V3.DMS.HLS +{ + public interface IM3U8Parser + { + /// + /// プレイリストを解析する + /// + /// + /// + IEnumerable Parse(string text); + } + + public class M3U8Parser : IM3U8Parser + { + public IEnumerable Parse(string text) + { + var nodes = new List(); + var lines = text.Split(Environment.NewLine); + if (lines.Length == 1) + { + lines = text.Split("\n"); + } + + var index = 0; + + while (index < lines.Length) + { + var line = lines[index]; + if (line.StartsWith("#EXT-X-STREAM-INF:")) + { + var streamInfo = new M3U8Node(M3U8NodeType.StreamInfo, line[(line.IndexOf(":") + 1)..], lines[index + 1]); + nodes.Add(streamInfo); + index += 2; + } + else if (line.StartsWith("#EXT-X-KEY:")) + { + var streamInfo = new M3U8Node(M3U8NodeType.Key, line[(line.IndexOf(":") + 1)..], string.Empty); + nodes.Add(streamInfo); + index += 1; + } + else if (line.StartsWith("#EXT-X-MEDIA:")) + { + var streamInfo = new M3U8Node(M3U8NodeType.Audio, line[(line.IndexOf(":") + 1)..], string.Empty); + nodes.Add(streamInfo); + index += 1; + } + else if (line.StartsWith("#EXTINF:")) + { + + var streamInfo = new M3U8Node(M3U8NodeType.Segment, line[(line.IndexOf(":") + 1)..], lines[index + 1]); + nodes.Add(streamInfo); + index += 2; + } + else if (line.StartsWith("#EXT-X-MAP")) + { + var streamInfo = new M3U8Node(M3U8NodeType.InitializationSection, line[(line.IndexOf("=") + 2)..^1], string.Empty); + nodes.Add(streamInfo); + index += 1; + } + else + { + index += 1; + } + } + + return nodes; + } + } +} diff --git a/Niconicome/Models/Domain/Niconico/Download/Video/V3/DMS/StreamCollection.cs b/Niconicome/Models/Domain/Niconico/Download/Video/V3/DMS/StreamCollection.cs new file mode 100644 index 00000000..9532d4df --- /dev/null +++ b/Niconicome/Models/Domain/Niconico/Download/Video/V3/DMS/StreamCollection.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Niconicome.Models.Domain.Niconico.Net.Json.API.Mylist; + +namespace Niconicome.Models.Domain.Niconico.Download.Video.V3.DMS +{ + public interface IStreamCollection + { + /// + /// 指定したresolutionのストリームを取得する + /// 存在しない場合、resolution以下の最大のストリームを取得する + /// + /// + /// + public IStreamInfo GetStream(int resolution); + + /// + /// ストリームの数 + /// + int Count { get; } + } + + public class StreamCollection : IStreamCollection + { + public StreamCollection(IEnumerable streams) + { + this.streams = streams; + } + + private readonly IEnumerable streams; + + public IStreamInfo GetStream(int resolution) + { + var stream = this.streams.Where(s => !s.IsLowest).Where(s => s.VerticalResolution == resolution).FirstOrDefault(); + if (stream is not null) return stream; + return this.streams.OrderByDescending(s => s.VerticalResolution).First(s => s.VerticalResolution <= resolution); + } + + public int Count => this.streams.Count(); + } +} diff --git a/Niconicome/Models/Domain/Niconico/Download/Video/V3/DMS/StreamInfo.cs b/Niconicome/Models/Domain/Niconico/Download/Video/V3/DMS/StreamInfo.cs new file mode 100644 index 00000000..1c63a1d4 --- /dev/null +++ b/Niconicome/Models/Domain/Niconico/Download/Video/V3/DMS/StreamInfo.cs @@ -0,0 +1,270 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Niconicome.Models.Domain.Niconico.Download.Video.V3.DMS.HLS; +using Niconicome.Models.Domain.Niconico.Download.Video.V3.Error; +using Niconicome.Models.Domain.Utils.Error; +using Niconicome.Models.Helper.Result; + +namespace Niconicome.Models.Domain.Niconico.Download.Video.V3.DMS +{ + public interface IStreamInfo + { + /// + /// 垂直方向解像度 + /// + int VerticalResolution { get; } + + /// + /// Lowest + /// + bool IsLowest { get; } + + /// + /// プレイリストのURL + /// + public string PlaylistURL { get; } + + /// + /// 音声のURL + /// + public string AudioURL { get; } + + /// + /// セグメントのURL + /// + IEnumerable VideoSegmentURLs { get; } + + /// + /// 音声セグメントのURL + /// + IEnumerable AudioSegmentURLs { get; } + + /// + /// 動画セグメントの長さ + /// + IEnumerable VideoSegmentDurations { get; } + + /// + /// 音声セグメントの長さ + /// + IEnumerable AudioSegmentDurations { get; } + + /// + /// キーのURL + /// + string VideoKeyURL { get; } + + /// + /// IV + /// + string VideoIV { get; } + + /// + /// 音声キーのURL + /// + string AudioKeyURL { get; } + + /// + /// 音声のIV + /// + string AudioIV { get; } + + /// + /// 動画の初期化情報URL + /// + string VideoInitializationURL { get; } + + /// + /// 恩賜絵の + /// + string AudioInitializationURL { get; } + + /// + /// 帯域 + /// + int VideoBandWidth { get; } + + /// + /// 初期化 + /// + /// + /// + /// + void Initialize(string playlistURL, string audioURL, int verticalResolution, bool isLowest,int bandWidth); + + /// + /// 情報を取得 + /// + Task GetStreamInfo(); + } + + public record SegmentDuration(string Filename, float Duration); + + public class StreamInfo : IStreamInfo + { + public StreamInfo(INicoHttp httpHandler, IM3U8Parser m3U8Parser, IErrorHandler errorHandler) + { + this._httpHandler = httpHandler; + this._m3U8Parser = m3U8Parser; + this._errorHandler = errorHandler; + } + + public string PlaylistURL { get; private set; } = string.Empty; + + public string AudioURL { get; private set; } = string.Empty; + + private readonly INicoHttp _httpHandler; + + private readonly IM3U8Parser _m3U8Parser; + + private readonly IErrorHandler _errorHandler; + + public int VerticalResolution { get; private set; } + + public bool IsLowest { get; private set; } + + public IEnumerable VideoSegmentURLs { get; private set; } = new List(); + + public IEnumerable AudioSegmentURLs { get; private set; } = new List(); + + public IEnumerable VideoSegmentDurations { get; private set; } = new List(); + + public IEnumerable AudioSegmentDurations { get; private set; } = new List(); + + public string VideoKeyURL { get; private set; } = string.Empty; + + public string VideoIV { get; private set; } = string.Empty; + + public string AudioKeyURL { get; private set; } = string.Empty; + + public string AudioIV { get; private set; } = string.Empty; + + public string VideoInitializationURL { get; private set; } = string.Empty; + + public string AudioInitializationURL { get; private set; } = string.Empty; + + public int VideoBandWidth { get; private set; } + + + public void Initialize(string playlistURL, string audioURL, int verticalResolution, bool isLowest, int bandWidth) + { + this.PlaylistURL = playlistURL; + this.AudioURL = audioURL; + this.VerticalResolution = verticalResolution; + this.IsLowest = isLowest; + this.VideoBandWidth = bandWidth; + } + + + public async Task GetStreamInfo() + { + IAttemptResult vResult = await this.GetContent(this.PlaylistURL); + if (!vResult.IsSucceeded || vResult.Data is null) + { + return vResult; + } + + IAttemptResult aResult = await this.GetContent(this.AudioURL); + if (!aResult.IsSucceeded || aResult.Data is null) + { + return aResult; + } + + SegmentInfo video = this.Parse(vResult.Data); + SegmentInfo audio = this.Parse(aResult.Data); + + this.AudioSegmentURLs = audio.segmentURL; + this.VideoSegmentURLs = video.segmentURL; + + this.VideoSegmentDurations = new List(video.segmentDurations.Select((duration, index) => new SegmentDuration(Path.GetFileName(new Uri(video.segmentURL[index]).AbsolutePath), duration))); + this.AudioSegmentDurations = new List(audio.segmentDurations.Select((duration, index) => new SegmentDuration(Path.GetFileName(new Uri(audio.segmentURL[index]).AbsolutePath), duration))); + + this.VideoKeyURL = video.keyURL; + this.VideoIV = video.IV; + this.AudioKeyURL = audio.keyURL; + this.AudioIV = audio.IV; + this.VideoInitializationURL = video.InitializeURL; + this.AudioInitializationURL = audio.InitializeURL; + + return AttemptResult.Succeeded(); + + } + + private async Task> GetContent(string url) + { + try + { + var res = await this._httpHandler.GetAsync(new Uri(url)); + if (!res.IsSuccessStatusCode) + { + return AttemptResult.Fail(this._errorHandler.HandleError(StreamParserError.FailedToGetPlaylistWithHttpError, (int)res.StatusCode, PlaylistURL)); + } + + return AttemptResult.Succeeded(await res.Content.ReadAsStringAsync()); + + } + catch (Exception ex) + { + return AttemptResult.Fail(this._errorHandler.HandleError(StreamParserError.FailedToGetPlaylist, ex, ex.Message)); + } + } + + /// + /// プレイリストを解析 + /// + /// + /// + private SegmentInfo Parse(string content) + { + IEnumerable nodes = this._m3U8Parser.Parse(content); + var streams = new List(); + var durations = new List(); + string key = string.Empty; + string IV = string.Empty; + string initialization = string.Empty; + + foreach (var node in nodes) + { + if (node.Type == M3U8NodeType.Segment) + { + streams.Add(node.URL); + if (float.TryParse(node.Value[0..^1], out float duration)) + { + durations.Add(duration); + } + else + { + durations.Add(6); + } + } + else if (node.Type == M3U8NodeType.Key) + { + var audioInfo = node.Value.Split(","); + var dict = new Dictionary(); + foreach (var info in audioInfo) + { + int equalIndex = info.IndexOf("="); + dict.Add(info[0..equalIndex], info[(equalIndex + 1)..]); + } + + key = dict["URI"][1..^1]; + IV = dict["IV"]; + } + else if (node.Type == M3U8NodeType.InitializationSection) + { + initialization = node.Value; + } + } + + return new SegmentInfo(streams.AsReadOnly(), durations.AsReadOnly(), initialization, key, IV); + } + + private record SegmentInfo(IReadOnlyList segmentURL, IReadOnlyList segmentDurations, string InitializeURL, string keyURL, string IV); + + + } +} diff --git a/Niconicome/Models/Domain/Niconico/Download/Video/V3/DMS/StreamParser.cs b/Niconicome/Models/Domain/Niconico/Download/Video/V3/DMS/StreamParser.cs new file mode 100644 index 00000000..e5bc3b3a --- /dev/null +++ b/Niconicome/Models/Domain/Niconico/Download/Video/V3/DMS/StreamParser.cs @@ -0,0 +1,113 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Niconicome.Models.Domain.Utils; +using Error = Niconicome.Models.Domain.Utils.Error; +using Niconicome.Models.Helper.Result; +using Niconicome.Models.Domain.Niconico.Download.Video.V3.DMS.HLS; +using Niconicome.Models.Domain.Niconico.Download.Video.V3.Error; + +namespace Niconicome.Models.Domain.Niconico.Download.Video.V3.DMS +{ + public interface IStreamParser + { + /// + /// master.m3u8をパースする + /// 子プレイリストはmaster.m3u8のURLから取得する + /// + /// + /// + Task> ParseAsync(string masterURL); + } + + public class StreamParser : IStreamParser + { + public StreamParser(INicoHttp httpHandler, Error::IErrorHandler errorHandler, IM3U8Parser m3U8Parser) + { + this._httpHandler = httpHandler; + this._errorHandler = errorHandler; + this._m3U8Parser = m3U8Parser; + } + + + private readonly INicoHttp _httpHandler; + + private readonly Error::IErrorHandler _errorHandler; + + private readonly IM3U8Parser _m3U8Parser; + + public async Task> ParseAsync(string masterURL) + { + string content; + + try + { + var res = await this._httpHandler.GetAsync(new Uri(masterURL)); + if (!res.IsSuccessStatusCode) + { + return AttemptResult.Fail(this._errorHandler.HandleError(StreamParserError.FailedToGetPlaylistWithHttpError, (int)res.StatusCode, masterURL)); + } + + content = await res.Content.ReadAsStringAsync(); + + } + catch (Exception ex) + { + return AttemptResult.Fail(this._errorHandler.HandleError(StreamParserError.FailedToGetPlaylist, ex, ex.Message)); + } + + IEnumerable nodes = this._m3U8Parser.Parse(content); + var streams = new List(); + var audios = new Dictionary(); + + foreach (var node in nodes) + { + if (node.Type == M3U8NodeType.StreamInfo) + { + var streamInfo = node.Value.Split(","); + var dict = new Dictionary(); + foreach (var info in streamInfo) + { + var kv = info.Split("="); + if (kv.Length != 2) continue; + dict.Add(kv[0], kv[1]); + } + + int bandWidth = int.Parse(dict["BANDWIDTH"]); + var resolution = this.GetVerticalResolution(dict["RESOLUTION"]); + var playlistURL = node.URL; + var stream = DIFactory.Resolve(); + stream.Initialize(playlistURL, audios[dict["AUDIO"]], resolution, playlistURL.Contains("lowest"), bandWidth); + streams.Add(stream); + } + else if (node.Type == M3U8NodeType.Audio) + { + var streamInfo = node.Value.Split(","); + var dict = new Dictionary(); + foreach (var info in streamInfo) + { + dict.Add(info[0..info.IndexOf("=")], info[(info.IndexOf("=") + 1)..]); + } + + audios.Add(dict["GROUP-ID"], dict["URI"][1..^1]); + + } + } + + return AttemptResult.Succeeded(new StreamCollection(streams)); + + } + + /// + /// 垂直方向の解像度をuintで取得する関数 + /// + /// + /// + private int GetVerticalResolution(string resolution) + { + var res = resolution.Split("x"); + return int.Parse(res[1]); + } + + } +} diff --git a/Niconicome/Models/Domain/Niconico/Download/Video/V3/Error/KeyDownlaoderError.cs b/Niconicome/Models/Domain/Niconico/Download/Video/V3/Error/KeyDownlaoderError.cs new file mode 100644 index 00000000..13f36d8e --- /dev/null +++ b/Niconicome/Models/Domain/Niconico/Download/Video/V3/Error/KeyDownlaoderError.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Niconicome.Models.Domain.Utils.Error; + +namespace Niconicome.Models.Domain.Niconico.Download.Video.V3.Error +{ + public enum KeyDownlaoderError + { + [ErrorEnum(ErrorLevel.Error,"キーのダウンロードに失敗しました。")] + FailedToDownloadKey, + } +} diff --git a/Niconicome/Models/Domain/Niconico/Download/Video/V3/Error/SegmentDirectoryHandlerError.cs b/Niconicome/Models/Domain/Niconico/Download/Video/V3/Error/SegmentDirectoryHandlerError.cs new file mode 100644 index 00000000..98865b8a --- /dev/null +++ b/Niconicome/Models/Domain/Niconico/Download/Video/V3/Error/SegmentDirectoryHandlerError.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Niconicome.Models.Domain.Utils.Error; + +namespace Niconicome.Models.Domain.Niconico.Download.Video.V3.Error +{ + public enum SegmentDirectoryHandlerError + { + [ErrorEnum(ErrorLevel.Error, "指定されたセグメントディレクトリが存在しません。(id:{0}, resolution:{1})")] + NotExists, + [ErrorEnum(ErrorLevel.Error, "不正なディレクトリ名です。(path:{0})")] + InvalidDirectoryName, + } +} diff --git a/Niconicome/Models/Domain/Niconico/Download/Video/V3/Error/SegmentDownloaderError.cs b/Niconicome/Models/Domain/Niconico/Download/Video/V3/Error/SegmentDownloaderError.cs new file mode 100644 index 00000000..5856ef6b --- /dev/null +++ b/Niconicome/Models/Domain/Niconico/Download/Video/V3/Error/SegmentDownloaderError.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Niconicome.Models.Domain.Utils.Error; + +namespace Niconicome.Models.Domain.Niconico.Download.Video.V3.Error +{ + public enum SegmentDownloaderError + { + [ErrorEnum(ErrorLevel.Error, "未初期化です。")] + NotInitialized, + [ErrorEnum(ErrorLevel.Error, "セグメントファイルのダウンロード処理がキャンセルされました。(id:{0})")] + Canceled, + [ErrorEnum(ErrorLevel.Error, "セグメントファイルのいずれかのダウンロードに失敗したため、処理を中止します。(id:{0})")] + FailedInAny, + [ErrorEnum(ErrorLevel.Error, "セグメント(idx:{0})の取得に失敗しました。(status:{1}, url:{2}, id:{3}")] + FailedToFetch, + [ErrorEnum(ErrorLevel.Log, "セグメント(idx:{0})を取得しました。(id:{1})")] + SucceededToFetch, + } +} diff --git a/Niconicome/Models/Domain/Niconico/Download/Video/V3/Error/SegmentWriterError.cs b/Niconicome/Models/Domain/Niconico/Download/Video/V3/Error/SegmentWriterError.cs new file mode 100644 index 00000000..228c3625 --- /dev/null +++ b/Niconicome/Models/Domain/Niconico/Download/Video/V3/Error/SegmentWriterError.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Niconicome.Models.Domain.Utils.Error; + +namespace Niconicome.Models.Domain.Niconico.Download.Video.V3.Error +{ + public enum SegmentWriterError + { + [ErrorEnum(ErrorLevel.Error, "セグメントファイルの保存先ディレクトリパスの取得に失敗しました。(path:{0})")] + FailedToGetDirPath, + } +} diff --git a/Niconicome/Models/Domain/Niconico/Download/Video/V3/Error/StreamJsonHandlerError.cs b/Niconicome/Models/Domain/Niconico/Download/Video/V3/Error/StreamJsonHandlerError.cs new file mode 100644 index 00000000..fb654207 --- /dev/null +++ b/Niconicome/Models/Domain/Niconico/Download/Video/V3/Error/StreamJsonHandlerError.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Niconicome.Models.Domain.Utils.Error; + +namespace Niconicome.Models.Domain.Niconico.Download.Video.V3.Error +{ + public enum StreamJsonHandlerError + { + [ErrorEnum(ErrorLevel.Error,"stream.jsonが存在しません。")] + FileNotExists, + } +} diff --git a/Niconicome/Models/Domain/Niconico/Download/Video/V3/Error/StreamParserError.cs b/Niconicome/Models/Domain/Niconico/Download/Video/V3/Error/StreamParserError.cs new file mode 100644 index 00000000..f83e50ef --- /dev/null +++ b/Niconicome/Models/Domain/Niconico/Download/Video/V3/Error/StreamParserError.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Niconicome.Models.Domain.Utils.Error; + +namespace Niconicome.Models.Domain.Niconico.Download.Video.V3.Error +{ + public enum StreamParserError + { + [ErrorEnum(ErrorLevel.Error, "ストリーム情報の取得に失敗しました。(status:{0}, url:{1})")] + FailedToGetPlaylistWithHttpError, + [ErrorEnum(ErrorLevel.Error, "ストリーム情報の取得に失敗しました。(詳細:{0})")] + FailedToGetPlaylist, + } +} diff --git a/Niconicome/Models/Domain/Niconico/Download/Video/V3/Error/WatchSessionError.cs b/Niconicome/Models/Domain/Niconico/Download/Video/V3/Error/WatchSessionError.cs new file mode 100644 index 00000000..3ef70bdd --- /dev/null +++ b/Niconicome/Models/Domain/Niconico/Download/Video/V3/Error/WatchSessionError.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Niconicome.Models.Domain.Utils.Error; + +namespace Niconicome.Models.Domain.Niconico.Download.Video.V3.Error +{ + public enum WatchSessionError + { + [ErrorEnum(ErrorLevel.Error,"セッションが失効しています。")] + SessionExpired, + [ErrorEnum(ErrorLevel.Error,"セッションが確立されていません。")] + SessionNotEnsured, + [ErrorEnum(ErrorLevel.Error,"暗号化された動画のため、ダウンロードできません。(id:{0})")] + VideoIsEncrypted, + [ErrorEnum(ErrorLevel.Error, "有料動画のため、ダウンロードできません。(id:{0})")] + VideoRequirePayment, + [ErrorEnum(ErrorLevel.Error, "セッションの確立に失敗しました。(id:{0})")] + SessionEnsuringFailure, + [ErrorEnum(ErrorLevel.Log, "{0}の視聴セッションを確立しました。")] + SessionEnsured, + [ErrorEnum(ErrorLevel.Error, "ハートビートの送信に失敗しました。(session_id:{0},status: {0})")] + FailedToSendHeartBeat, + [ErrorEnum(ErrorLevel.Log, "ハートビートの送信に成功しました。(session_id:{0})")] + SucceededToSendHeartBeat, + [ErrorEnum(ErrorLevel.Error,"アドオンが不正な情報を返却しました。(id:{0})")] + AddonReturnedInvalidInfomation, + [ErrorEnum(ErrorLevel.Error,"セッション確立アドオンが登録されていません。")] + AddonNotRegistered, + } +} diff --git a/Niconicome/Models/Domain/Niconico/Download/Video/V3/External/ExternalDownloaderHandler.cs b/Niconicome/Models/Domain/Niconico/Download/Video/V3/External/ExternalDownloaderHandler.cs new file mode 100644 index 00000000..93ab0b90 --- /dev/null +++ b/Niconicome/Models/Domain/Niconico/Download/Video/V3/External/ExternalDownloaderHandler.cs @@ -0,0 +1,105 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Niconicome.Models.Const; +using Niconicome.Models.Domain.Local.External.Software.NiconicomeProcess; +using Niconicome.Models.Domain.Local.Settings; +using Niconicome.Models.Domain.Local.Settings.Enum; +using Niconicome.Models.Domain.Niconico.Video.Infomations; +using Niconicome.Models.Helper.Result; +using Niconicome.Models.Network.Download; + +namespace Niconicome.Models.Domain.Niconico.Download.Video.V3.External +{ + public interface IExternalDownloaderHandler + { + /// + /// 外部ダウンローダーでDLすべきかどうかを判断する + /// + /// + /// + /// + bool CheckCondition(IDomainVideoInfo videoInfo, IDownloadSettings settings); + + /// + /// 外部ダウンローダーでDL + /// + /// + /// + /// + /// + /// + /// + Task DownloadVideoByExtarnalDownloaderAsync(IDownloadSettings settings, string niconicoID, string outputPath, Action onMessage, CancellationToken token); + } + + public class ExternalDownloaderHandler : IExternalDownloaderHandler + { + public ExternalDownloaderHandler(ISettingsContainer settingsContainer, IProcessManager processManager) + { + this._settingsContainer = settingsContainer; + this._processManager = processManager; + } + + #region field + + private readonly ISettingsContainer _settingsContainer; + + private readonly IProcessManager _processManager; + + #endregion + + #region Method + + public bool CheckCondition(IDomainVideoInfo videoInfo, IDownloadSettings settings) + { + + + ExternalDownloaderConditionSetting setting = settings.ExternalDownloaderConditionSetting; + + if (setting == ExternalDownloaderConditionSetting.Always) + { + return true; + } + else if (setting == ExternalDownloaderConditionSetting.Encrypted) + { + return videoInfo.DmcInfo.IsEncrypted; + } + else if (setting == ExternalDownloaderConditionSetting.Official) + { + return videoInfo.DmcInfo.IsOfficial; + } + else + { + return false; + } + } + + public async Task DownloadVideoByExtarnalDownloaderAsync(IDownloadSettings settings, string niconicoID, string outputPath, Action onMessage, CancellationToken token) + { + + string path = settings.ExternalDownloaderPath; + string param = settings.ExternalDownloaderParam + .Replace("", niconicoID) + .Replace("", NetConstant.NiconicoWatchUrl + niconicoID) + .Replace("", outputPath); + + IAttemptResult result = await this._processManager.StartProcessAsync(path, param, true, onMessage, token); + + if (result.IsSucceeded) + { + return AttemptResult.Succeeded(result.Message); + } + else + { + return AttemptResult.Fail(result.Message); + } + + } + + #endregion + } +} diff --git a/Niconicome/Models/Domain/Niconico/Download/Video/V3/Fetch/Key/KeyDownlaoder.cs b/Niconicome/Models/Domain/Niconico/Download/Video/V3/Fetch/Key/KeyDownlaoder.cs new file mode 100644 index 00000000..a7617cc0 --- /dev/null +++ b/Niconicome/Models/Domain/Niconico/Download/Video/V3/Fetch/Key/KeyDownlaoder.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Niconicome.Models.Domain.Utils.Error; +using Niconicome.Models.Helper.Result; + +namespace Niconicome.Models.Domain.Niconico.Download.Video.V3.Fetch.Key +{ + public interface IKeyDownlaoder + { + /// + /// Keyをダウンロードする + /// + /// + /// + /// + Task> DownloadKeyASync(string videoKeyURL, string audioKeyURL); + } + + public class KeyDownlaoder : IKeyDownlaoder + { + public KeyDownlaoder(INicoHttp http, IErrorHandler errorHandler) + { + this._http = http; + this._errorHandler = errorHandler; + } + + #region field + + private readonly INicoHttp _http; + private readonly IErrorHandler _errorHandler; + + #endregion + + public async Task> DownloadKeyASync(string videoKeyURL, string audioKeyURL) + { + string videoKey; + string audioKey; + + try + { + var res = await this._http.GetAsync(new Uri(videoKeyURL)); + videoKey = Convert.ToBase64String(await res.Content.ReadAsByteArrayAsync()); + } + catch (Exception ex) + { + return AttemptResult.Fail(this._errorHandler.HandleError(Error.KeyDownlaoderError.FailedToDownloadKey, ex)); + } + + try + { + var res = await this._http.GetAsync(new Uri(audioKeyURL)); + audioKey = Convert.ToBase64String(await res.Content.ReadAsByteArrayAsync()); + } + catch (Exception ex) + { + return AttemptResult.Fail(this._errorHandler.HandleError(Error.KeyDownlaoderError.FailedToDownloadKey, ex)); + } + + + + return AttemptResult.Succeeded(new KeyInfomation(videoKey,audioKey)); + } + } +} diff --git a/Niconicome/Models/Domain/Niconico/Download/Video/V3/Fetch/Key/KeyInfomation.cs b/Niconicome/Models/Domain/Niconico/Download/Video/V3/Fetch/Key/KeyInfomation.cs new file mode 100644 index 00000000..250371c1 --- /dev/null +++ b/Niconicome/Models/Domain/Niconico/Download/Video/V3/Fetch/Key/KeyInfomation.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Niconicome.Models.Domain.Niconico.Download.Video.V3.Fetch.Key +{ + public record KeyInfomation(string VideoKey,string AudioKey); +} diff --git a/Niconicome/Models/Domain/Niconico/Download/Video/V3/Fetch/Segment/SegmentDLResultContainer.cs b/Niconicome/Models/Domain/Niconico/Download/Video/V3/Fetch/Segment/SegmentDLResultContainer.cs new file mode 100644 index 00000000..ee8958d6 --- /dev/null +++ b/Niconicome/Models/Domain/Niconico/Download/Video/V3/Fetch/Segment/SegmentDLResultContainer.cs @@ -0,0 +1,82 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Niconicome.Models.Domain.Niconico.Download.Video.V3.Fetch.Segment +{ + public interface ISegmentDLResultContainer + { + /// + /// 結果をセット + /// + /// + /// + void SetResult(bool result, int index); + + /// + /// 結果を確認 + /// + bool IsAllSucceeded { get; } + + /// + /// 失敗を確認 + /// + bool IsFailedInAny { get; } + + /// + /// セグメント数 + /// + int Length { get; } + + /// + /// 完了数 + /// + int CompletedCount { get; } + + } + + public class SegmentDLResultContainer : ISegmentDLResultContainer + { + public SegmentDLResultContainer(int length) + { + this._result = Enumerable.Range(0, length).Select(_ => true).ToArray(); + } + + #region field + + private readonly object _lock = new(); + + private readonly bool[] _result; + + #endregion + + #region Props + + public bool IsAllSucceeded => this._result.All(r => r); + + public bool IsFailedInAny => this._result.Any(r => !r); + + public int Length => this._result.Length; + + public int CompletedCount { get; private set; } = 0; + + #endregion + + #region Method + + public void SetResult(bool result, int index) + { + lock (this._lock) + { + this.CompletedCount++; + } + + this._result[index] = result; + } + + + #endregion + } +} diff --git a/Niconicome/Models/Domain/Niconico/Download/Video/V3/Fetch/Segment/SegmentDownloader.cs b/Niconicome/Models/Domain/Niconico/Download/Video/V3/Fetch/Segment/SegmentDownloader.cs new file mode 100644 index 00000000..f3098694 --- /dev/null +++ b/Niconicome/Models/Domain/Niconico/Download/Video/V3/Fetch/Segment/SegmentDownloader.cs @@ -0,0 +1,157 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Niconicome.Models.Domain.Utils.Error; +using Niconicome.Models.Domain.Utils.StringHandler; +using Niconicome.Models.Helper.Result; +using Err = Niconicome.Models.Domain.Niconico.Download.Video.V3.Error.SegmentDownloaderError; +using SC = Niconicome.Models.Domain.Niconico.Download.Video.V3.Fetch.Segment.StringContent.SegmentDownloaderSC; + +namespace Niconicome.Models.Domain.Niconico.Download.Video.V3.Fetch.Segment +{ + + public interface ISegmentDownloader + { + /// + /// セグメントをダウンロードする + /// + /// + Task DownloadAsync(); + + /// + /// 初期化 + /// + /// + /// + void Initialize(ISegmentInfomation infomation, ISegmentDLResultContainer container); + } + + public class SegmentDownloader : ISegmentDownloader + { + public SegmentDownloader(INicoHttp http, ISegmentWriter writer, IErrorHandler errorHandler, IStringHandler stringHandler) + { + this._http = http; + this._writer = writer; + this._errorHandler = errorHandler; + this._stringHandler = stringHandler; + } + + #region field + + private readonly INicoHttp _http; + + private readonly ISegmentWriter _writer; + + private readonly IErrorHandler _errorHandler; + + private readonly IStringHandler _stringHandler; + + private bool _isInitialized; + + private ISegmentDLResultContainer? _resultContainer; + + private ISegmentInfomation? _segmentInfomation; + + #endregion + + #region Method + + public async Task DownloadAsync() + { + if (!this._isInitialized) + { + this._errorHandler.HandleError(Err.NotInitialized); + return AttemptResult.Fail(this._errorHandler.GetMessageForResult(Err.NotInitialized)); + } + + //中止処理 + if (this._segmentInfomation!.CT.IsCancellationRequested) + { + this._resultContainer!.SetResult(false, this._segmentInfomation.Index); + this._errorHandler.HandleError(Err.Canceled, this._segmentInfomation.NiconicoID); + return AttemptResult.Fail(this._errorHandler.GetMessageForResult(Err.Canceled, this._segmentInfomation.NiconicoID)); + } + + //他のセグメントのDLに失敗した場合は中止 + if (this._resultContainer!.IsFailedInAny) + { + this._resultContainer.SetResult(false, this._segmentInfomation.Index); + this._errorHandler.HandleError(Err.FailedInAny, this._segmentInfomation.NiconicoID); + return AttemptResult.Fail(this._errorHandler.GetMessageForResult(Err.FailedInAny, this._segmentInfomation.NiconicoID)); + } + + //DL + IAttemptResult result = await this.DownloadInternalAsync(this._segmentInfomation); + + if (!result.IsSucceeded || result.Data is null) + { + this._resultContainer.SetResult(false, this._segmentInfomation.Index); + return AttemptResult.Fail(result.Message); + } + + this._errorHandler.HandleError(Err.SucceededToFetch, this._segmentInfomation.Index, this._segmentInfomation.NiconicoID); + + if (this._segmentInfomation.CT.IsCancellationRequested) + { + this._resultContainer!.SetResult(false, this._segmentInfomation.Index); + this._errorHandler.HandleError(Err.Canceled, this._segmentInfomation.NiconicoID); + return AttemptResult.Fail(this._errorHandler.GetMessageForResult(Err.Canceled, this._segmentInfomation.NiconicoID)); + } + + + //書き込み + IAttemptResult writeResult = this._writer.Write(result.Data, this._segmentInfomation.FilePath); + + if (!writeResult.IsSucceeded) + { + this._resultContainer.SetResult(false, this._segmentInfomation.Index); + return writeResult; + } + + this._resultContainer.SetResult(true, this._segmentInfomation.Index); + this._segmentInfomation.OnMessage(this._stringHandler.GetContent(SC.CompletedMessage, this._resultContainer.CompletedCount, this._resultContainer.Length, this._segmentInfomation.VerticalResolution)); + + return AttemptResult.Succeeded(); + + } + + public void Initialize(ISegmentInfomation infomation, ISegmentDLResultContainer container) + { + this._segmentInfomation = infomation; + this._resultContainer = container; + this._isInitialized = true; + } + + #endregion + + #region private + + private async Task> DownloadInternalAsync(ISegmentInfomation infomation, int retryAttempt = 0) + { + var res = await this._http.GetAsync(new Uri(infomation.SegmentURL)); + if (!res.IsSuccessStatusCode) + { + //リトライ回数は3回 + if (retryAttempt < 3) + { + retryAttempt++; + await Task.Delay(10 * 1000, infomation.CT); + return await this.DownloadInternalAsync(infomation, retryAttempt); + } + else + { + this._errorHandler.HandleError(Err.FailedToFetch, infomation.Index, (int)res.StatusCode, infomation.SegmentURL, infomation.NiconicoID); + return AttemptResult.Fail(this._errorHandler.GetMessageForResult(Err.FailedToFetch, infomation.Index, (int)res.StatusCode, infomation.SegmentURL, infomation.NiconicoID)); + } + } + + return AttemptResult.Succeeded(await res.Content.ReadAsByteArrayAsync()); + } + + #endregion + } +} diff --git a/Niconicome/Models/Domain/Niconico/Download/Video/V3/Fetch/Segment/SegmentInfomation.cs b/Niconicome/Models/Domain/Niconico/Download/Video/V3/Fetch/Segment/SegmentInfomation.cs new file mode 100644 index 00000000..5455987c --- /dev/null +++ b/Niconicome/Models/Domain/Niconico/Download/Video/V3/Fetch/Segment/SegmentInfomation.cs @@ -0,0 +1,95 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Niconicome.Models.Domain.Niconico.Download.Video.V3.Fetch.Segment +{ + public interface ISegmentInfomation + { + /// + /// メッセージハンドラ + /// + /// + void OnMessage(string message); + + /// + /// ID + /// + string NiconicoID { get; } + + /// + /// URL + /// + string SegmentURL { get; } + + /// + /// ファイルパス + /// + string FilePath { get; } + + /// + /// インデックス + /// + int Index { get; } + + /// + /// 解像度 + /// + int VerticalResolution { get; } + + /// + /// CT + /// + CancellationToken CT { get; } + } + + public class SegmentInfomation : ISegmentInfomation + { + public SegmentInfomation(Action onMessage, string segmentURL, int index, string filePath, int verticalResolution, string niconicoID, CancellationToken cT) + { + this._onMessage = onMessage; + this.SegmentURL = segmentURL; + this.Index = index; + this.VerticalResolution = verticalResolution; + this.FilePath = filePath; + this.CT = cT; + this.NiconicoID = niconicoID; + } + + #region field + + private readonly Action _onMessage; + + #endregion + + #region Props + + public string NiconicoID { get; init; } + + public string SegmentURL { get; init; } + + public string FilePath { get; init; } + + public int Index { get; init; } + + public int VerticalResolution { get; init; } + + public CancellationToken CT { get; init; } + + + #endregion + + #region Method + + public void OnMessage(string message) + { + this._onMessage(message); + } + + + #endregion + } +} diff --git a/Niconicome/Models/Domain/Niconico/Download/Video/V3/Fetch/Segment/SegmentWriter.cs b/Niconicome/Models/Domain/Niconico/Download/Video/V3/Fetch/Segment/SegmentWriter.cs new file mode 100644 index 00000000..baab5ddc --- /dev/null +++ b/Niconicome/Models/Domain/Niconico/Download/Video/V3/Fetch/Segment/SegmentWriter.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Niconicome.Models.Domain.Local.IO.V2; +using Niconicome.Models.Domain.Utils.Error; +using Niconicome.Models.Helper.Result; +using Err = Niconicome.Models.Domain.Niconico.Download.Video.V3.Error.SegmentWriterError; + +namespace Niconicome.Models.Domain.Niconico.Download.Video.V3.Fetch.Segment +{ + public interface ISegmentWriter + { + /// + /// セグメントファイルを書き込む + /// + /// + /// + /// + IAttemptResult Write(byte[] data, string path); + } + + public class SegmentWriter : ISegmentWriter + { + public SegmentWriter(IErrorHandler errorHandler,INiconicomeFileIO fileIO,INiconicomeDirectoryIO directoryIO) + { + this._errorHandler = errorHandler; + this._fileIO = fileIO; + this._directoryIO = directoryIO; + } + + #region field + + private readonly IErrorHandler _errorHandler; + + private readonly INiconicomeFileIO _fileIO; + + private readonly INiconicomeDirectoryIO _directoryIO; + + #endregion + + #region Method + + public IAttemptResult Write(byte[] data, string path) + { + string? dirPath = Path.GetDirectoryName(path); + + if (string.IsNullOrEmpty(dirPath)) + { + this._errorHandler.HandleError(Err.FailedToGetDirPath, path); + return AttemptResult.Fail(this._errorHandler.GetMessageForResult(Err.FailedToGetDirPath, path)); + } + + if (!this._directoryIO.Exists(dirPath)) + { + IAttemptResult dirResult = this._directoryIO.CreateDirectory(dirPath); + if (!dirResult.IsSucceeded) + { + return dirResult; + } + } + + IAttemptResult result = this._fileIO.Write(path,data); + + return result; + } + + #endregion + } +} diff --git a/Niconicome/Models/Domain/Niconico/Download/Video/V3/Fetch/Segment/StringContent/SegmentDownloaderSC.cs b/Niconicome/Models/Domain/Niconico/Download/Video/V3/Fetch/Segment/StringContent/SegmentDownloaderSC.cs new file mode 100644 index 00000000..7a6772b1 --- /dev/null +++ b/Niconicome/Models/Domain/Niconico/Download/Video/V3/Fetch/Segment/StringContent/SegmentDownloaderSC.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Niconicome.Models.Domain.Utils.StringHandler; + +namespace Niconicome.Models.Domain.Niconico.Download.Video.V3.Fetch.Segment.StringContent +{ + public enum SegmentDownloaderSC + { + [StringEnum("完了: {0}/{1} {2}px")] + CompletedMessage, + } +} diff --git a/Niconicome/Models/Domain/Niconico/Download/Video/V3/Integrate/VideoDownloader.cs b/Niconicome/Models/Domain/Niconico/Download/Video/V3/Integrate/VideoDownloader.cs new file mode 100644 index 00000000..07938fca --- /dev/null +++ b/Niconicome/Models/Domain/Niconico/Download/Video/V3/Integrate/VideoDownloader.cs @@ -0,0 +1,369 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using System.Windows.Input; +using Niconicome.Models.Const; +using Niconicome.Models.Domain.Local.External.Software.NiconicomeProcess; +using Niconicome.Models.Domain.Local.IO.V2; +using Niconicome.Models.Domain.Local.Store.V2; +using Niconicome.Models.Domain.Niconico.Download.Video.V3.Fetch.Segment; +using Niconicome.Models.Domain.Niconico.Download.Video.V3.Session; +using Niconicome.Models.Domain.Niconico.Video.Infomations; +using Niconicome.Models.Domain.Utils; +using Niconicome.Models.Domain.Utils.StringHandler; +using Niconicome.Models.Helper.Result; +using Niconicome.Models.Network.Download; +using Niconicome.Models.Utils.ParallelTaskV2; +using SC = Niconicome.Models.Domain.Niconico.Download.Video.V3.Integrate.VideoDownloaderSC; +using Niconicome.Models.Domain.Niconico.Download.Video.V3.External; +using Niconicome.Models.Domain.Niconico.Download.Video.V3.Local.DMS; +using Niconicome.Models.Domain.Niconico.Download.Video.V3.DMS; +using Niconicome.Models.Domain.Niconico.Download.Video.V3.Fetch.Key; +using Niconicome.Models.Domain.Niconico.Download.Video.V3.Local.StreamJson; +using System.Text.RegularExpressions; + +namespace Niconicome.Models.Domain.Niconico.Download.Video.V3.Integrate +{ + public interface IVideoDownloader + { + /// + /// 動画をダウンロード + /// + /// + /// + /// + /// + /// + Task> DownloadVideoAsync(IDownloadSettings settings, Action OnMessage, IDomainVideoInfo videoInfo, CancellationToken token); + + } + + public class VideoDownloader : IVideoDownloader + { + public VideoDownloader(IPathOrganizer pathOrganizer, ISegmentDirectoryHandler segmentDirectory, INiconicomeFileIO fileIO, IStringHandler stringHandler, INiconicomeDirectoryIO directoryIO, IExternalDownloaderHandler external, IKeyDownlaoder keyDownlaoder, IStreamJsonHandler streamJsonHandler) + { + this._pathOrganizer = pathOrganizer; + this._segmentDirectory = segmentDirectory; + this._fileIO = fileIO; + this._stringHandler = stringHandler; + this._directoryIO = directoryIO; + this._external = external; + this._keyDownlaoder = keyDownlaoder; + this._streamJsonHandler = streamJsonHandler; + } + + #region field + + private readonly IPathOrganizer _pathOrganizer; + + private readonly ISegmentDirectoryHandler _segmentDirectory; + + private readonly INiconicomeFileIO _fileIO; + + private readonly IStringHandler _stringHandler; + + private readonly INiconicomeDirectoryIO _directoryIO; + + private readonly IExternalDownloaderHandler _external; + + private readonly IKeyDownlaoder _keyDownlaoder; + + private readonly IStreamJsonHandler _streamJsonHandler; + + #endregion + + #region Method + + public async Task> DownloadVideoAsync(IDownloadSettings settings, Action OnMessage, IDomainVideoInfo videoInfo, CancellationToken token) + { + //DLするかどうかを判定 + if (!this.ShouldDownladVideo(videoInfo, settings)) + { + OnMessage(this._stringHandler.GetContent(SC.SkipEconomy)); + return AttemptResult.Succeeded(0); + } + + //ファイルパス + string filePath = this._pathOrganizer.GetFilePath(settings.FileNameFormat, videoInfo.DmcInfo, string.Empty, settings.FolderPath, settings.IsReplaceStrictedEnable, settings.Overwrite); + + if (!this._directoryIO.Exists(filePath)) + { + this._directoryIO.CreateDirectory(filePath); + } + + //外部ダウンローダー + if (this._external.CheckCondition(videoInfo, settings)) + { + IAttemptResult externalResult = await this._external.DownloadVideoByExtarnalDownloaderAsync(settings, videoInfo.Id, filePath, OnMessage, token); + if (!externalResult.IsSucceeded) + { + return AttemptResult.Fail(externalResult.Message); + } + + IAttemptResult resolutionResult = await this._fileIO.GetVerticalResolutionAsync(filePath); + if (!resolutionResult.IsSucceeded) + { + return AttemptResult.Succeeded(0); + } + else + { + return AttemptResult.Succeeded(resolutionResult.Data); + } + } + + using IWatchSession session = DIFactory.Resolve(); + + //視聴セッションを確立 + OnMessage(this._stringHandler.GetContent(SC.EnsureSession)); + IAttemptResult sessionResult = await session.EnsureSessionAsync(videoInfo); + + if (!sessionResult.IsSucceeded) + { + return AttemptResult.Fail(sessionResult.Message); + } + + //ストリーム情報を取得 + OnMessage(this._stringHandler.GetContent(SC.FetchingStreamInfo)); + IAttemptResult streamResult = await session.GetAvailableStreamsAsync(); + if (!streamResult.IsSucceeded || streamResult.Data is null) + { + return AttemptResult.Fail(streamResult.Message); + } + IStreamInfo stream = streamResult.Data.GetStream((int)settings.VerticalResolution); + + IAttemptResult streamInfoResult = await stream.GetStreamInfo(); + if (!streamInfoResult.IsSucceeded) + { + return AttemptResult.Fail(streamInfoResult.Message); + } + + //レジューム + string tempFolderPath; + IEnumerable existingVideoFileNames; + IEnumerable existingAudioFileNames; + + IAttemptResult resumeResult = settings.ResumeEnable ? this.GetResumeInfomation(videoInfo.Id, stream.VerticalResolution) : AttemptResult.Fail(); + + if (!resumeResult.IsSucceeded || resumeResult.Data is null) + { + IAttemptResult segmentResult = this._segmentDirectory.Create(videoInfo.Id, stream.VerticalResolution); + if (!segmentResult.IsSucceeded || segmentResult.Data is null) + { + return AttemptResult.Fail(segmentResult.Message); + } + + + tempFolderPath = segmentResult.Data; + existingAudioFileNames = new List(); + existingVideoFileNames = new List(); + } + else + { + OnMessage(this._stringHandler.GetContent(SC.Resume)); + + tempFolderPath = resumeResult.Data.SegmentDirectoryPath; + existingAudioFileNames = resumeResult.Data.ExistingAudioFileNames; + existingVideoFileNames = resumeResult.Data.ExistingVideoFileNames; + } + + + //セグメントのDL + IAttemptResult dlResult = await this.DownloadSegments(stream.VideoSegmentURLs.Concat([stream.VideoInitializationURL]), stream.AudioSegmentURLs.Concat([stream.AudioInitializationURL]), existingVideoFileNames, existingAudioFileNames, tempFolderPath, videoInfo.Id, stream.VerticalResolution, settings.MaxParallelSegmentDLCount, OnMessage, token); + if (!dlResult.IsSucceeded) + { + return AttemptResult.Fail(dlResult.Message); + } + + //ファイルを移動 + string destination = Path.Combine(filePath, stream.VerticalResolution.ToString()); + if (!this._directoryIO.Exists(destination)) + { + this._directoryIO.CreateDirectory(destination); + } + IAttemptResult moveResult = this.MoveFiles(tempFolderPath, Path.Combine(filePath, stream.VerticalResolution.ToString())); + if (!moveResult.IsSucceeded) + { + return AttemptResult.Fail(moveResult.Message); + } + + //キー + IAttemptResult keyResult = await this._keyDownlaoder.DownloadKeyASync(stream.VideoKeyURL, stream.AudioKeyURL); + if (!keyResult.IsSucceeded || keyResult.Data is null) + { + return AttemptResult.Fail(keyResult.Message); + } + + //動画情報JSON + + string vMap = Path.GetFileName(new Uri(stream.VideoInitializationURL).AbsolutePath); + string aMap = Path.GetFileName(new Uri(stream.AudioInitializationURL).AbsolutePath); + + + IAttemptResult jsonResult = this._streamJsonHandler.AddStream(filePath, stream.VerticalResolution, keyResult.Data.VideoKey, keyResult.Data.AudioKey, stream.VideoIV, stream.AudioIV, stream.VideoSegmentDurations, stream.AudioSegmentDurations, vMap, aMap, stream.VideoBandWidth); + if (!jsonResult.IsSucceeded) + { + return AttemptResult.Fail(jsonResult.Message); + } + + + //一時フォルダーを削除 + this._directoryIO.Delete(tempFolderPath); + + OnMessage(this._stringHandler.GetContent(SC.Completed)); + return AttemptResult.Succeeded(stream.VerticalResolution); + } + + + #endregion + + #region field + + /// + /// セグメントをダウンロードする + /// + /// + /// + /// + /// + /// + /// + /// + /// + private async Task DownloadSegments(IEnumerable videos, IEnumerable audios, IEnumerable existingVideoFileNames, IEnumerable existingAudioFileNames, string folderPath, string videoID, int verticalResoluiton, int parallelDLCount, Action onMessage, CancellationToken token) + { + var targets = videos.Concat(audios).ToList(); + var handler = new ParallelTasksHandler(parallelDLCount); + var container = new SegmentDLResultContainer(targets.Count); + + foreach (var streamURL in targets) + + { + var fileNameWIthoutExt = Path.GetFileNameWithoutExtension(streamURL); + var fileName = Path.GetFileName(new Uri(streamURL).AbsolutePath); + var index = 0; + if (!fileNameWIthoutExt.Contains("init")) + { + index = int.Parse(fileNameWIthoutExt.TrimStart('0')) - 1; + } + else + { + index = videos.Count() - 1; + } + string localPath; + + if (streamURL.Contains(".cmfv")) + { + localPath = Path.Combine(folderPath, "video", fileName); + if (existingVideoFileNames.Contains(fileName)) + { + container.SetResult(true, index); + } + } + else + { + localPath = Path.Combine(folderPath, "audio", fileName); + index += videos.Count(); + if (existingAudioFileNames.Contains(fileName)) + { + container.SetResult(true, index); + } + } + + var info = new SegmentInfomation(onMessage, streamURL, index, localPath, verticalResoluiton, videoID, token); + + var task = new ParallelTask(async _ => + { + var downloader = DIFactory.Resolve(); + downloader.Initialize(info, container); + + await downloader.DownloadAsync(); + + }, _ => { }); + + handler.AddTaskToQueue(task); + } + + await handler.ProcessTasksAsync(); + + if (!container.IsAllSucceeded) + { + return AttemptResult.Fail(); + } + else + { + return AttemptResult.Succeeded(folderPath); + } + } + + /// + /// レジューム情報を取得する + /// + /// + /// + /// + private IAttemptResult GetResumeInfomation(string videoID, int verticalResoluiton) + { + if (!this._segmentDirectory.Exists(videoID, verticalResoluiton)) + { + return AttemptResult.Fail(); + } + + IAttemptResult resumeResult = this._segmentDirectory.GetSegmentDirectoryInfo(videoID, verticalResoluiton); + + if (resumeResult.IsSucceeded && resumeResult.Data is not null) + { + return AttemptResult.Succeeded(new ResumeInfomation(resumeResult.Data.ExistingVideoFileNames, resumeResult.Data.ExistingAudioFileNames, resumeResult.Data.DirectoryPath)); + } + else + { + return AttemptResult.Fail(resumeResult.Message); + } + } + + /// + /// ファイルを移動する + /// + /// + /// + /// + private IAttemptResult MoveFiles(string directoryPath, string destination) + { + IAttemptResult resultV = this._directoryIO.Move(Path.Combine(directoryPath, "video"), Path.Combine(destination, "video")); + + if (!resultV.IsSucceeded) return resultV; + + IAttemptResult resultA = this._directoryIO.Move(Path.Combine(directoryPath, "audio"), Path.Combine(destination, "audio")); + + return resultA; + } + + /// + /// DLするかどうかを判定する + /// + /// + /// + /// + private bool ShouldDownladVideo(IDomainVideoInfo videoInfo, IDownloadSettings settings) + { + if (!videoInfo.DmcInfo.IsEconomy) return true; + + if (string.IsNullOrEmpty(settings.FilePath)) return true; + + if (!this._fileIO.Exists(settings.FilePath)) return true; + + if (settings.SkipEconomyDownloadIfPremiumExists && !settings.FilePath.Contains(settings.EconomySuffix)) return false; + + if (settings.AlwaysSkipEconomyDownload) return false; + + return true; + } + + + private record ResumeInfomation(IEnumerable ExistingVideoFileNames, IEnumerable ExistingAudioFileNames, string SegmentDirectoryPath); + + #endregion + } +} diff --git a/Niconicome/Models/Domain/Niconico/Download/Video/V3/Integrate/VideoDownloaderSC.cs b/Niconicome/Models/Domain/Niconico/Download/Video/V3/Integrate/VideoDownloaderSC.cs new file mode 100644 index 00000000..11298833 --- /dev/null +++ b/Niconicome/Models/Domain/Niconico/Download/Video/V3/Integrate/VideoDownloaderSC.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Niconicome.Models.Domain.Utils.StringHandler; + +namespace Niconicome.Models.Domain.Niconico.Download.Video.V3.Integrate +{ + public enum VideoDownloaderSC + { + [StringEnum("視聴セッションを確立中...")] + EnsureSession, + [StringEnum("ストリーム情報を取得中...")] + FetchingStreamInfo, + [StringEnum("DLをレジューム")] + Resume, + [StringEnum("完了: {0}/{1} {2}px")] + SegmentDownloadCompleted, + [StringEnum("エンコード中...")] + Encode, + [StringEnum("動画のダウンロードが完了")] + Completed, + [StringEnum("エコノミー設定によりスキップ")] + SkipEconomy, + } +} diff --git a/Niconicome/Models/Domain/Niconico/Download/Video/V3/Local/DMS/DMSFileHandler.cs b/Niconicome/Models/Domain/Niconico/Download/Video/V3/Local/DMS/DMSFileHandler.cs new file mode 100644 index 00000000..18c3a091 --- /dev/null +++ b/Niconicome/Models/Domain/Niconico/Download/Video/V3/Local/DMS/DMSFileHandler.cs @@ -0,0 +1,81 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Niconicome.Models.Domain.Local.IO; +using System.IO; +using Niconicome.Extensions.System; +using Niconicome.Models.Domain.Local.IO.V2; + +namespace Niconicome.Models.Domain.Niconico.Download.Video.V3.Local.DMS +{ + public interface IDMSFileHandler + { + /// + /// 動画ファイルの存在を確認 + /// + /// + /// + /// + bool Exists(string id, string dirPath); + + /// + /// エコノミーであるかどうかを確認 + /// + /// + /// + /// + bool IsEconomy(string id, string dirPath); + } + + public class DMSFileHandler : IDMSFileHandler + { + public DMSFileHandler(INiconicomeDirectoryIO directoryIO) + { + this._directoryIO = directoryIO; + } + + private readonly INiconicomeDirectoryIO _directoryIO; + + public bool Exists(string id, string dirPath) + { + + var videoDir = Path.Combine(dirPath, id, "video"); + var audioDir = Path.Combine(dirPath, id, "audio"); + + if (!this._directoryIO.Exists(videoDir) || !this._directoryIO.Exists(audioDir)) return false; + + var videoSegments = this._directoryIO.GetDirectories(videoDir); + var audioSegments = this._directoryIO.GetDirectories(audioDir); + + if (videoSegments.Data is null || audioSegments.Data is null) return false; + + return videoSegments.Data.Any() && audioSegments.Data.Any(); + } + + public bool IsEconomy(string id, string dirPath) + { + + var videoDir = Path.Combine(dirPath, id, "video"); + + var _360p = Path.Combine(videoDir, "360"); + var _144p = Path.Combine(videoDir, "144"); + + + if (this._directoryIO.Exists(_360p) || this._directoryIO.Exists(_144p)) + { + var videoSegments = this._directoryIO.GetDirectories(videoDir); + + if (!videoSegments.IsSucceeded || videoSegments.Data is null) return false; + + return videoSegments.Data.Select(p => Path.GetFileName(p)).Where(p => p != "360" && p != "144").Any().Not(); + } + else + { + return false; + } + } + } + +} diff --git a/Niconicome/Models/Domain/Niconico/Download/Video/V3/Local/DMS/SegmentDirectoryHandler.cs b/Niconicome/Models/Domain/Niconico/Download/Video/V3/Local/DMS/SegmentDirectoryHandler.cs new file mode 100644 index 00000000..11cbf732 --- /dev/null +++ b/Niconicome/Models/Domain/Niconico/Download/Video/V3/Local/DMS/SegmentDirectoryHandler.cs @@ -0,0 +1,209 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Niconicome.Models.Const; +using Niconicome.Models.Domain.Local.IO.V2; +using Niconicome.Models.Domain.Utils.Error; +using Niconicome.Models.Helper.Result; +using Err = Niconicome.Models.Domain.Niconico.Download.Video.V3.Error.SegmentDirectoryHandlerError; + +namespace Niconicome.Models.Domain.Niconico.Download.Video.V3.Local.DMS +{ + public interface ISegmentDirectoryHandler + { + /// + /// セグメントファイルの保存先ディレクトリが既に存在するかどうかを確認 + /// + /// + /// + /// + bool Exists(string niconicoID, int resolution); + + /// + /// ディレクトリを作成 + /// + /// + /// + /// + IAttemptResult Create(string niconicoID, int resolution); + + /// + /// セグメントファイルの保存先ディレクトリ情報を取得 + /// + /// + /// + /// + IAttemptResult GetSegmentDirectoryInfo(string niconicoID, int resolution); + + /// + /// 全てのセグメントファイルの保存先ディレクトリを取得 + /// + /// + IAttemptResult> GetAllSegmentDirectoryInfos(); + + /// + /// セグメントファイルの保存先ディレクトリのルートを作成 + /// + /// + IAttemptResult CreateRootDirectotyIfNotExists(); + } + + internal class SegmentDirectoryHandler : ISegmentDirectoryHandler + { + public SegmentDirectoryHandler(INiconicomeDirectoryIO directoryIO, IErrorHandler errorHandler) + { + this._directoryIO = directoryIO; + this._errorHandler = errorHandler; + } + + #region field + + private readonly INiconicomeDirectoryIO _directoryIO; + + private readonly IErrorHandler _errorHandler; + + #endregion + + #region Method + + public bool Exists(string niconicoID, int resolution) + { + + IAttemptResult> dirResult = this._directoryIO.GetDirectories(Path.Combine(AppContext.BaseDirectory, FileFolder.SegmentsFolderPath)); + + if (!dirResult.IsSucceeded || dirResult.Data is null) + { + return false; + } + + string? directory = dirResult.Data.Select(p => Path.GetFileName(p)).FirstOrDefault(p => p.StartsWith($"{niconicoID}-{resolution}")); + + return directory is not null; + } + + public IAttemptResult Create(string niconicoID, int resolution) + { + string path = Path.Combine(AppContext.BaseDirectory, FileFolder.SegmentsFolderPath, $"{niconicoID}-{resolution}-{DateTime.Now.ToString("yyyy-MM-dd")}"); + + string videoPath = Path.Combine(path, "video"); + string audioPath = Path.Combine(path, "audio"); + + IAttemptResult result = this._directoryIO.CreateDirectory(path); + IAttemptResult vResult = this._directoryIO.CreateDirectory(videoPath); + IAttemptResult aResult = this._directoryIO.CreateDirectory(audioPath); + + if (new[] { result, vResult, aResult }.Any(p => !p.IsSucceeded)) + { + return AttemptResult.Fail(new[] { result, vResult, aResult }.First(r => !r.IsSucceeded).Message); + } + + return AttemptResult.Succeeded(path); + } + + public IAttemptResult GetSegmentDirectoryInfo(string niconicoID, int resolution) + { + if (!this.Exists(niconicoID, resolution)) + { + this._errorHandler.HandleError(Err.NotExists, niconicoID, resolution); + return AttemptResult.Fail(this._errorHandler.GetMessageForResult(Err.NotExists, niconicoID, resolution)); + } + + IAttemptResult> dirResult = this._directoryIO.GetDirectories(Path.Combine(AppContext.BaseDirectory, FileFolder.SegmentsFolderPath)); + + if (!dirResult.IsSucceeded || dirResult.Data is null) + { + return AttemptResult.Fail(dirResult.Message); + } + + string directoryName = dirResult.Data.Select(p => Path.GetFileName(p)).First(p => p.StartsWith($"{niconicoID}-{resolution}")); + + return this.Parse(directoryName); + } + + public IAttemptResult> GetAllSegmentDirectoryInfos() + { + IAttemptResult> dirResult = this._directoryIO.GetDirectories(Path.Combine(AppContext.BaseDirectory, FileFolder.SegmentsFolderPath)); + + if (!dirResult.IsSucceeded || dirResult.Data is null) + { + return AttemptResult>.Fail(dirResult.Message); + } + + var infos = new List(); + + foreach (var dir in dirResult.Data.Select(p => Path.GetFileName(p))) + { + IAttemptResult result = this.Parse(dir); + if (!result.IsSucceeded || result.Data is null) + { + continue; + } + + infos.Add(result.Data); + } + + return AttemptResult>.Succeeded(infos); + } + + public IAttemptResult CreateRootDirectotyIfNotExists() + { + string path = Path.Combine(AppContext.BaseDirectory, FileFolder.SegmentsFolderPath); + if (this._directoryIO.Exists(path)) + { + return AttemptResult.Succeeded(); + } + + return this._directoryIO.CreateDirectory(path); + } + + + + #endregion + + #region private + + private IAttemptResult Parse(string directoryName) + { + string[] splited = directoryName.Split("-"); + + if (splited.Length != 5) + { + this._errorHandler.HandleError(Err.InvalidDirectoryName, directoryName); + return AttemptResult.Fail(this._errorHandler.GetMessageForResult(Err.InvalidDirectoryName, directoryName)); + } + + string dt = $"{splited[2]}-{splited[3]}-{splited[4]}"; + bool parse = DateTime.TryParseExact(dt, "yyyy-MM-dd", null, DateTimeStyles.AssumeLocal, out DateTime result); + + if (!parse) + { + this._errorHandler.HandleError(Err.InvalidDirectoryName, directoryName); + return AttemptResult.Fail(this._errorHandler.GetMessageForResult(Err.InvalidDirectoryName, directoryName)); + } + + string dirPath = Path.Combine(AppContext.BaseDirectory, FileFolder.SegmentsFolderPath, directoryName); + IAttemptResult> videofilesResult = this._directoryIO.GetFiles(Path.Combine(dirPath, "video"), "*.cmfv"); + if (!videofilesResult.IsSucceeded || videofilesResult.Data is null) + { + return AttemptResult.Fail(videofilesResult.Message); + } + + IAttemptResult> audiofilesResult = this._directoryIO.GetFiles(Path.Combine(dirPath, "audio"), "*.cmfa"); + if (!audiofilesResult.IsSucceeded || audiofilesResult.Data is null) + { + return AttemptResult.Fail(audiofilesResult.Message); + } + + + + + return AttemptResult.Succeeded(new SegmentDirectoryInfo(dirPath, result, videofilesResult.Data.Select(p => Path.GetFileName(p)).ToList().AsReadOnly(), audiofilesResult.Data.Select(p => Path.GetFileName(p)).ToList().AsReadOnly())); + } + + #endregion + } +} diff --git a/Niconicome/Models/Domain/Niconico/Download/Video/V3/Local/DMS/SegmentDirectoryInfo.cs b/Niconicome/Models/Domain/Niconico/Download/Video/V3/Local/DMS/SegmentDirectoryInfo.cs new file mode 100644 index 00000000..d6787add --- /dev/null +++ b/Niconicome/Models/Domain/Niconico/Download/Video/V3/Local/DMS/SegmentDirectoryInfo.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Niconicome.Models.Domain.Niconico.Download.Video.V3.Local.DMS +{ + public interface ISegmentDirectoryInfo + { + /// + /// ディレクトリパス + /// + string DirectoryPath { get; } + + /// + /// DL開始日時 + /// + DateTime DownloadStartedOn { get; } + + /// + /// 存在するセグメントファイル名 + /// + IReadOnlyCollection ExistingVideoFileNames { get; } + + /// + /// 存在する音声ファイル名 + /// + IReadOnlyCollection ExistingAudioFileNames { get; } + } + + public record SegmentDirectoryInfo(string DirectoryPath, DateTime DownloadStartedOn, IReadOnlyCollection ExistingVideoFileNames, IReadOnlyCollection ExistingAudioFileNames) : ISegmentDirectoryInfo; +} diff --git a/Niconicome/Models/Domain/Niconico/Download/Video/V3/Local/StreamJson/StreamJsonHandler.cs b/Niconicome/Models/Domain/Niconico/Download/Video/V3/Local/StreamJson/StreamJsonHandler.cs new file mode 100644 index 00000000..3bd16823 --- /dev/null +++ b/Niconicome/Models/Domain/Niconico/Download/Video/V3/Local/StreamJson/StreamJsonHandler.cs @@ -0,0 +1,148 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Niconicome.Models.Domain.Local.IO.V2; +using Niconicome.Models.Domain.Niconico.Download.Video.V3.DMS; +using Niconicome.Models.Domain.Niconico.Net.Json; +using Niconicome.Models.Domain.Utils.Error; +using Niconicome.Models.Helper.Result; +using Err = Niconicome.Models.Domain.Niconico.Download.Video.V3.Error.StreamJsonHandlerError; +using Watch = Niconicome.Models.Domain.Local.Server.API.Watch.V1.LocalFile; + +namespace Niconicome.Models.Domain.Niconico.Download.Video.V3.Local.StreamJson +{ + public interface IStreamJsonHandler : Watch::ILocalFileInfoHandler + { + IAttemptResult AddStream(string folderPath, int resolution, string videoKey, string audioKey, string videoIV, string audioIV, IEnumerable videoSegments, IEnumerable audioSegments, string videoMapFileName, string audioMapFileName, int bandWidth); + } + + public class StreamJsonHandler : IStreamJsonHandler + { + public StreamJsonHandler(INiconicomeFileIO fileIO, IErrorHandler errorHandler) + { + this._fileIO = fileIO; + this._errorHandler = errorHandler; + } + + #region field + + private readonly INiconicomeFileIO _fileIO; + + private readonly IErrorHandler _errorHandler; + + #endregion + + public IAttemptResult AddStream(string folderPath, int resolution, string videoKey, string audioKey, string videoIV, string audioIV, IEnumerable videoSegments, IEnumerable audioSegments, string videoMapFileName, string audioMapFileName, int bandWidth) + { + var path = Path.Combine(folderPath, "stream.json"); + StreamType info; + + if (this._fileIO.Exists(path)) + { + IAttemptResult readResult = this._fileIO.Read(path); + if (!readResult.IsSucceeded || readResult.Data is null) + { + return readResult; + } + + info = JsonParser.DeSerialize(readResult.Data); + } + else + { + info = new StreamType(); + } + + Stream stream; + + if (info.Streams.Any(s => s.Resolution == resolution)) + { + stream = info.Streams.First(s => s.Resolution == resolution); + } + else + { + stream = new Stream(); + info.Streams.Add(stream); + } + + stream.Resolution = resolution; + stream.VideoKey = videoKey; + stream.AudioKey = audioKey; + stream.VideoIV = videoIV; + stream.AudioIV = audioIV; + stream.AudioMapFileName = audioMapFileName; + stream.VideoMapFileName = videoMapFileName; + stream.VideoBandWidth = bandWidth; + stream.VideoSegments = videoSegments.Select(s => new Segment() { Duration = s.Duration.ToString("N3"), FileName = s.Filename }).ToList(); + stream.AudioSegments = audioSegments.Select(s => new Segment() { Duration = s.Duration.ToString("N3"), FileName = s.Filename }).ToList(); + + return this._fileIO.Write(path, JsonParser.Serialize(info)); + } + + public IAttemptResult GetLocalFileInfo(string filePath) + { + if (!this._fileIO.Exists(filePath)) + { + return AttemptResult.Fail(this._errorHandler.HandleError(Err.FileNotExists)); + } + + IAttemptResult readResult = this._fileIO.Read(filePath); + if (!readResult.IsSucceeded || readResult.Data is null) + { + return AttemptResult.Fail(readResult.Message); + } + + var data = JsonParser.DeSerialize(readResult.Data); + var info = new LocalFileInfo(); + + foreach (var stream in data.Streams) + { + info.Streams.Add(stream.Resolution, new StreamInfo(stream)); + } + + return AttemptResult.Succeeded(info); + + } + } + + public class LocalFileInfo : Watch::ILocalFileInfo + { + public Dictionary Streams { get; init; } = new(); + } + + public class StreamInfo : Watch::IStreamInfo + { + public StreamInfo(Stream stream) + { + this.Resolution = stream.Resolution; + this.VideoKey = stream.VideoKey; + this.AudioKey = stream.AudioKey; + this.VideoIV = stream.VideoIV; + this.AudioIV = stream.AudioIV; + this.VideoMapFileName = stream.VideoMapFileName; + this.AudioMapFileName = stream.AudioMapFileName; + this.VideoBandWidth = stream.VideoBandWidth; + this.VideoSegments = stream.VideoSegments; + this.AudioSegments = stream.AudioSegments; + } + + public int Resolution { get; init; } + + public string VideoKey { get; init; } + + public string AudioKey { get; init; } + + public string VideoIV { get; init; } + + public string AudioIV { get; init; } + + public string VideoMapFileName { get; init; } + + public string AudioMapFileName { get; init; } + + public int VideoBandWidth { get; init; } + + public IEnumerable VideoSegments { get; init; } + + public IEnumerable AudioSegments { get; init; } + } +} diff --git a/Niconicome/Models/Domain/Niconico/Download/Video/V3/Local/StreamJson/StreamType.cs b/Niconicome/Models/Domain/Niconico/Download/Video/V3/Local/StreamJson/StreamType.cs new file mode 100644 index 00000000..263e8244 --- /dev/null +++ b/Niconicome/Models/Domain/Niconico/Download/Video/V3/Local/StreamJson/StreamType.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Niconicome.Models.Domain.Local.Server.API.Watch.V1.LocalFile; + +namespace Niconicome.Models.Domain.Niconico.Download.Video.V3.Local.StreamJson +{ + public class StreamType + { + public List Streams { get; set; } = new(); + } + + public class Stream + { + public int Resolution { get; set; } + + public string VideoKey { get; set; } = string.Empty; + + public string AudioKey { get; set; } = string.Empty; + + public string VideoIV { get; set; } = string.Empty; + + public string AudioIV { get; set; } = string.Empty; + + public string VideoMapFileName { get; set; } = string.Empty; + + public string AudioMapFileName { get; set; } = string.Empty; + + public int VideoBandWidth { get; set; } + + public List VideoSegments { get; set; } = new List(); + + public List AudioSegments { get; set; } = new List(); + + } + + public class Segment : ISegmentInfo + { + public string FileName { get; set; } = string.Empty; + + public string Duration { get; set; } = string.Empty; + } +} diff --git a/Niconicome/Models/Domain/Niconico/Download/Video/V3/Session/WatchSession.cs b/Niconicome/Models/Domain/Niconico/Download/Video/V3/Session/WatchSession.cs new file mode 100644 index 00000000..cba26679 --- /dev/null +++ b/Niconicome/Models/Domain/Niconico/Download/Video/V3/Session/WatchSession.cs @@ -0,0 +1,212 @@ +using System; +using System.Net.Http; +using System.Threading.Tasks; +using Niconicome.Extensions.System; +using Niconicome.Models.Domain.Local.Addons.API.Hooks; +using Niconicome.Models.Domain.Niconico.Download.Video.V3.DMS; +using Niconicome.Models.Domain.Niconico.Video.Infomations; +using Niconicome.Models.Domain.Niconico.Watch; +using Niconicome.Models.Domain.Utils.Error; +using Niconicome.Models.Helper.Result; +using Niconicome.Models.Local.State.MessageV2; +using Err = Niconicome.Models.Domain.Niconico.Download.Video.V3.Error.WatchSessionError; +using Utils = Niconicome.Models.Domain.Utils; + +namespace Niconicome.Models.Domain.Niconico.Download.Video.V3.Session +{ + public interface IWatchSession : IDisposable + { + /// + /// セッション確立フラグ + /// + bool IsSessionEnsured { get; } + + /// + /// セッション失効フラグ + /// + bool IsSessionExipired { get; } + + /// + /// セッションを確立する + /// + /// + /// + Task EnsureSessionAsync(IDomainVideoInfo videoInfo); + + /// + /// 取得可能なStreamの一覧 + /// + /// + Task> GetAvailableStreamsAsync(); + } + + /// + /// 視聴セッション全般を管理する + /// + public class WatchSession : IWatchSession + { + public WatchSession(IWatchInfohandler watchInfo, INicoHttp http,IStreamParser streamParser, IHooksManager hooksManager, IErrorHandler errorHandler) + { + this._watchInfo = watchInfo; + this._http = http; + this._hooksManager = hooksManager; + this._errorHandler = errorHandler; + this._streamParser = streamParser; + } + + ~WatchSession() + { + this.IsSessionEnsured = false; + } + + #region field + + private readonly IErrorHandler _errorHandler; + + private readonly IWatchInfohandler _watchInfo; + + private readonly IStreamParser _streamParser; + + private readonly INicoHttp _http; + + private readonly IHooksManager _hooksManager; + + private IWatchSessionInfo? _session; + + #endregion + + #region Props + + public bool IsSessionEnsured { get; private set; } + + public bool IsSessionExipired { get; private set; } + + #endregion + + #region Method + + public async Task EnsureSessionAsync(IDomainVideoInfo videoInfo) + { + //ダウンロード不可能な場合は処理をキャンセル + if (!videoInfo.DmcInfo.IsDownloadable) + { + if (videoInfo.DmcInfo.IsEncrypted) + { + this._errorHandler.HandleError(Err.VideoIsEncrypted, videoInfo.Id); + return AttemptResult.Fail(this._errorHandler.GetMessageForResult(Err.VideoIsEncrypted, videoInfo.Id)); + } + else + { + this._errorHandler.HandleError(Err.VideoRequirePayment, videoInfo.Id); + return AttemptResult.Fail(this._errorHandler.GetMessageForResult(Err.VideoRequirePayment, videoInfo.Id)); + } + } + + + //セッション確立 + if (!this._hooksManager.IsRegistered(HookType.SessionEnsuring)) + { + return AttemptResult.Fail(this._errorHandler.HandleError(Err.AddonNotRegistered)); + } + + IAttemptResult result =await this.EnsureSessionWithAddonAsync(videoInfo); + + if (!result.IsSucceeded || result.Data is null) + { + return AttemptResult.Fail(result.Message); + } + else + { + this._session = result.Data; + } + + this.IsSessionEnsured = true; + + this._errorHandler.HandleError(Err.SessionEnsured, videoInfo.Id); + + return AttemptResult.Succeeded(); + } + + public async Task> GetAvailableStreamsAsync() + { + if (this.IsSessionExipired) + { + this._errorHandler.HandleError(Err.SessionExpired); + return AttemptResult.Fail(this._errorHandler.GetMessageForResult(Err.SessionExpired)); + } + + if (this._session is null) + { + this._errorHandler.HandleError(Err.SessionNotEnsured); + return AttemptResult.Fail(this._errorHandler.GetMessageForResult(Err.SessionNotEnsured)); + } + + return await this._streamParser.ParseAsync(this._session.ContentUrl); + + } + + /// + /// セッションを破棄する + /// + public void Dispose() + { + this.IsSessionEnsured = false; + this.IsSessionExipired = true; + GC.SuppressFinalize(this); + } + + #endregion + + #region private + + /// + /// アドオンでセッションを確立する + /// + /// + /// + private async Task> EnsureSessionWithAddonAsync(IDomainVideoInfo video) + { + IAttemptResult result = await this._hooksManager.EnsureSessionAsync(video.RawDmcInfo); + if (!result.IsSucceeded || result.Data is null) + { + if (result.Exception is not null) + { + this._errorHandler.HandleError(Err.SessionEnsuringFailure, result.Exception, video.Id); + return AttemptResult.Fail(this._errorHandler.GetMessageForResult(Err.SessionEnsuringFailure, result.Exception, video.Id)); + } + else + { + this._errorHandler.HandleError(Err.SessionEnsuringFailure, video.Id); + return AttemptResult.Fail(this._errorHandler.GetMessageForResult(Err.SessionEnsuringFailure, video.Id)); + } + } + + try + { + if (result.Data.DmcResponseJsonData is not string jsonData || result.Data.ContentUrl is not string contentUrl || result.Data.SessionId is not string sessionID || result.Data.IsDMS is not bool isDMS) + { + this._errorHandler.HandleError(Err.AddonReturnedInvalidInfomation, video.Id); + return AttemptResult.Fail(this._errorHandler.GetMessageForResult(Err.AddonReturnedInvalidInfomation, video.Id)); + } + + var info = new WatchSessionInfo() + { + DmcResponseJsonData = jsonData, + ContentUrl = contentUrl, + SessionId = sessionID, + IsDMS = isDMS, + }; + + return new AttemptResult() { IsSucceeded = true, Data = info }; + } + catch (Exception ex) + { + this._errorHandler.HandleError(Err.AddonReturnedInvalidInfomation, ex, video.Id); + return AttemptResult.Fail(this._errorHandler.GetMessageForResult(Err.AddonReturnedInvalidInfomation, ex, video.Id)); + } + } + + #endregion + } + +} diff --git a/Niconicome/Models/Domain/Niconico/NicoHttp.cs b/Niconicome/Models/Domain/Niconico/NicoHttp.cs index 56a79aa1..de1c2015 100644 --- a/Niconicome/Models/Domain/Niconico/NicoHttp.cs +++ b/Niconicome/Models/Domain/Niconico/NicoHttp.cs @@ -1,8 +1,13 @@ using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Net; using System.Net.Http; +using System.Net.Security; using System.Reflection; using System.Threading.Tasks; using Niconicome.Models.Domain.Local.Settings; +using Niconicome.Models.Domain.Utils; using Niconicome.Models.Helper.Result; using Windows.Media.Protection.PlayReady; using Const = Niconicome.Models.Const; @@ -32,7 +37,7 @@ public interface INicoHttp /// /// /// - Task PostAsync(Uri uri, HttpContent content); + Task PostAsync(Uri uri, HttpContent content, Dictionary? headers = null); /// /// Option @@ -61,13 +66,14 @@ public NicoHttp(HttpClient client, ISettingsContainer settingsContainer) if (!result.IsSucceeded || string.IsNullOrEmpty(result.Data)) { client.DefaultRequestHeaders.UserAgent.ParseAdd($"Mozilla/5.0 (Niconicome/{version?.Major}.{version?.Minor}.{version?.Build})"); - } else + } + else { client.DefaultRequestHeaders.UserAgent.ParseAdd(result.Data); } client.DefaultRequestHeaders.Referrer = new Uri(Const::NetConstant.NiconicoBaseURL); - client.DefaultRequestHeaders.Add("x-frontend-id", "6"); + client.DefaultRequestHeaders.Add("X-Frontend-id", "6"); client.DefaultRequestHeaders.Add("x-frontend-version", "0"); client.DefaultRequestHeaders.Add("x-client-os-type", "others"); @@ -79,12 +85,8 @@ public NicoHttp(HttpClient client, ISettingsContainer settingsContainer) public HttpRequestMessage CreateRequest(HttpMethod method, Uri url) { var m = new HttpRequestMessage(method, url); - var version = Assembly.GetExecutingAssembly().GetName().Version; - m.Headers.UserAgent.ParseAdd($"Mozilla/5.0 (Niconicome/{version?.Major}.{version?.Minor}.{version?.Build})"); m.Headers.Referrer = new Uri(Const::NetConstant.NiconicoBaseURL); - m.Headers.Add("x-frontend-id", "6"); - m.Headers.Add("x-frontend-version", "0"); - m.Headers.Add("x-client-os-type", "others"); + m.Headers.UserAgent.ParseAdd(this._client.DefaultRequestHeaders.UserAgent.ToString()); return m; } @@ -99,9 +101,20 @@ public async Task GetAsync(Uri uri) return await this._client.GetAsync(uri); } - public async Task PostAsync(Uri uri, HttpContent content) + public async Task PostAsync(Uri uri, HttpContent content, Dictionary? headers = null) { - return await this._client.PostAsync(uri, content); + var message = this.CreateRequest(HttpMethod.Post, uri); + message.Content = content; + if (headers is not null) + { + foreach (var key in headers.Keys) + { + message.Headers.Add(key, headers[key]); + } + } + var cookie = DIFactory.Resolve(); + Debug.WriteLine(message); + return await this._client.SendAsync(message); } public async Task OptionAsync(Uri uri) @@ -129,4 +142,21 @@ public async Task SendAsync(HttpRequestMessage requestMessa #endregion } + public class NicoHttpClientHandler : HttpClientHandler + { + private readonly bool _skipSSL; + + public NicoHttpClientHandler(CookieContainer container, bool skip) + { + this.UseCookies = true; + this._skipSSL = skip; + this.CookieContainer = container; + this.ServerCertificateCustomValidationCallback = (sender, cert, chain, sslPolicyErrors) => + { + if (this._skipSSL) return true; + return sslPolicyErrors == SslPolicyErrors.None; + }; + } + } + } diff --git a/Niconicome/Models/Domain/Niconico/NiconicoContext.cs b/Niconicome/Models/Domain/Niconico/NiconicoContext.cs index a45bc3cc..1914e8f6 100644 --- a/Niconicome/Models/Domain/Niconico/NiconicoContext.cs +++ b/Niconicome/Models/Domain/Niconico/NiconicoContext.cs @@ -4,11 +4,16 @@ using System.Net.Http; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; +using Niconicome.Models.Domain.Local.Store.V2; using Niconicome.Models.Domain.Network; using Niconicome.Models.Domain.Niconico.Net.Xml; -using Niconicome.Models.Domain.Utils; +using Niconicome.Models.Domain.Niconico.UserAuth; +using Niconicome.Models.Domain.Utils.Error; +using Niconicome.Models.Helper.Result; +using Niconicome.Models.Utils.Reactive; using Reactive.Bindings; using Const = Niconicome.Models.Const; +using DI = Niconicome.Models.Domain.Utils.DIFactory; namespace Niconicome.Models.Domain.Niconico { @@ -17,20 +22,12 @@ public interface INiconicoContext /// /// ログイン状態 /// - bool IsLogin { get; } + IBindableProperty IsLogin { get; } /// /// ユーザー情報 /// - ReactiveProperty User { get; } - - /// - /// ログインする - /// - /// - /// - /// - Task LoginAsync(string u, string p); + User? User { get; } /// /// ログアウトする @@ -39,29 +36,21 @@ public interface INiconicoContext Task LogoutAsync(); /// - /// ページURLを取得する + /// 引数のCookieでログインして、Cookieを保存する /// - /// /// - Uri GetPageUri(string id); - - /// - /// ユーザー情報を更新 - /// - /// - Task RefreshUser(); + Task LoginAndSaveCookieAsync(string userSession, string userSessionSecure); } public class NiconicoContext : INiconicoContext { - public NiconicoContext(INicoHttp http, ICookieManager cookieManager, ILogger logger, INetWorkHelper helper) + public NiconicoContext(INicoHttp http, ICookieManager cookieManager, IErrorHandler errorHandler, ICookieStore cookieStore) { this._http = http; this._cookieManager = cookieManager; - this._logger = logger; - this._helper = helper; - this.User = new ReactiveProperty(); + this._errorHandler = errorHandler; + this._cookieStore = cookieStore; } #region field @@ -70,9 +59,9 @@ public NiconicoContext(INicoHttp http, ICookieManager cookieManager, ILogger log private readonly ICookieManager _cookieManager; - private readonly ILogger _logger; + private readonly IErrorHandler _errorHandler; - private readonly INetWorkHelper _helper; + private readonly ICookieStore _cookieStore; private readonly string UserNameAPI = "https://seiga.nicovideo.jp/api/user/info?id="; @@ -80,89 +69,91 @@ public NiconicoContext(INicoHttp http, ICookieManager cookieManager, ILogger log #region Props - public static INiconicoContext Context { get; private set; } = DIFactory.Provider.GetRequiredService(); + public User? User { get; private set; } - public ReactiveProperty User { get; init; } - - public bool IsLogin - { - get - { - return this._cookieManager.HasCookie("user_session"); - } - } + public IBindableProperty IsLogin { get; init; } = new BindableProperty(false); #endregion #region Method - public async Task LoginAsync(string username, string password) + public async Task LogoutAsync() { - if (this.IsLogin) return true; - - //Cookieを削除 + if (!this.IsLogin.Value) return; + await this._http.GetAsync(new Uri(Const::NetConstant.NiconicoLogoutURL)); this._cookieManager.DeleteAllCookies(); + this._cookieStore.DeleteCookieInfo(); + this.User = null; + this.IsLogin.Value = false; + } - //ログイン処理 - var data = new Dictionary() - { - {"mail_tel",username }, - {"password",password }, - {"next_url",null } - }; - - var formData = new FormUrlEncodedContent((IEnumerable>)data); - - HttpResponseMessage result = await this._http.PostAsync(new Uri(Const::NetConstant.NiconicoLoginURL), formData); + public async Task LoginAndSaveCookieAsync(string userSession, string userSessionSecure) + { + bool loginOK; - if (result.IsSuccessStatusCode || result.StatusCode == HttpStatusCode.Found) + try { - if (this._cookieManager.HasCookie("user_session")) - { - await this.RefreshUser(); - return true; - } - else - { - return false; - } + var result = await this._http.GetAsync(new Uri(@"https://nicovideo.jp/my")); + loginOK = result.IsSuccessStatusCode && result.StatusCode != HttpStatusCode.Found; } - else + catch (Exception ex) { - return false; + return AttemptResult.Fail(this._errorHandler.HandleError(NiconicoContextError.FailedToCheckLoginStatus, ex)); } - } + if (!loginOK) + { + return AttemptResult.Fail(this._errorHandler.HandleError(NiconicoContextError.InvalidCookie)); + } - public async Task LogoutAsync() - { - if (!this.IsLogin) return; - await this._http.GetAsync(new Uri(Const::NetConstant.NiconicoLogoutURL)); - this._cookieManager.DeleteAllCookies(); - this.User.Value = null; - } + IAttemptResult cookieResult = this._cookieStore.GetCookieInfo(); + if (!cookieResult.IsSucceeded || cookieResult.Data is null) return AttemptResult.Fail(cookieResult.Message); + ICookieInfo cookie = cookieResult.Data; + cookie.UserSession = userSession; + cookie.UserSessionSecure = userSessionSecure; - public async Task RefreshUser() - { - if (!this.IsLogin) return; - string userID = this._cookieManager.GetCookie("user_session").Split('_')[2]; - string userName = await this.GetUserNameAsync(userID) + "さん"; + this._cookieManager.AddCookie("user_session", userSession); + this._cookieManager.AddCookie("user_session_secure", userSessionSecure); - this.User.Value = new User(userName, userID); - } + string userID = userSession.Split("_")[2]; - public Uri GetPageUri(string id) - { - return new Uri($"https://nicovideo.jp/watch/{id}"); + IAttemptResult userNameResult = await this.GetUserNameAsync(userID); + + if (!userNameResult.IsSucceeded || userNameResult.Data is null) + { + return AttemptResult.Fail(userNameResult.Message); + } + + this.User = new User(userNameResult.Data, userID); + + this.IsLogin.Value = true; + + return AttemptResult.Succeeded(); } + #endregion #region private - private async Task GetUserNameAsync(string id) + + /// + /// ニコニコ静画のAPIを使ってユーザー名を取得する + /// + /// + /// + private async Task> GetUserNameAsync(string id) { - HttpResponseMessage result = await this._http.GetAsync(new Uri($"{this.UserNameAPI}{id}")); + HttpResponseMessage result; + + try + { + result = await this._http.GetAsync(new Uri($"{this.UserNameAPI}{id}")); + } + catch (Exception ex) + { + return AttemptResult.Fail(this._errorHandler.HandleError(NiconicoContextError.FailedToGetUserName, ex)); + } if (result.IsSuccessStatusCode) { @@ -173,26 +164,36 @@ private async Task GetUserNameAsync(string id) { xmlData = Xmlparser.Deserialize(response); } - catch + catch (Exception ex) { - return string.Empty; + return AttemptResult.Fail(this._errorHandler.HandleError(NiconicoContextError.FailedToGetUserName, ex)); } if (xmlData is null) { - return string.Empty; + return AttemptResult.Fail(this._errorHandler.HandleError(NiconicoContextError.FailedToGetUserName)); } - return xmlData.User.Nickname; + return AttemptResult.Succeeded(xmlData.User.Nickname); } else { - return string.Empty; + return AttemptResult.Fail(this._errorHandler.HandleError(NiconicoContextError.FailedToGetUserName)); } } #endregion } + + public enum NiconicoContextError + { + [ErrorEnum(ErrorLevel.Error, "有効なCookieではありません。")] + InvalidCookie, + [ErrorEnum(ErrorLevel.Error, "ログイン状態の確認に失敗しました。")] + FailedToCheckLoginStatus, + [ErrorEnum(ErrorLevel.Error, "ユーザー名の取得に失敗しました。")] + FailedToGetUserName, + } } diff --git a/Niconicome/Models/Domain/Niconico/Remote/V2/Error/SeriesError.cs b/Niconicome/Models/Domain/Niconico/Remote/V2/Error/SeriesError.cs index 6057351f..a89d6724 100644 --- a/Niconicome/Models/Domain/Niconico/Remote/V2/Error/SeriesError.cs +++ b/Niconicome/Models/Domain/Niconico/Remote/V2/Error/SeriesError.cs @@ -19,5 +19,7 @@ public enum SeriesError DataAnalysisFailed, [ErrorEnum(ErrorLevel.Log, "{0}件の動画を「{1}」から取得しました。")] RetrievingHasCompleted, + [ErrorEnum(ErrorLevel.Error, "ユーザーIDの取得に失敗しました。(series:{0})")] + FailedToGetUserID, } } diff --git a/Niconicome/Models/Domain/Niconico/UserAuth/CookiInfo.cs b/Niconicome/Models/Domain/Niconico/UserAuth/CookiInfo.cs new file mode 100644 index 00000000..4d6e0c59 --- /dev/null +++ b/Niconicome/Models/Domain/Niconico/UserAuth/CookiInfo.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Text; +using System.Threading.Tasks; +using Niconicome.Extensions.System; +using Niconicome.Models.Domain.Local.Store.V2; + +namespace Niconicome.Models.Domain.Niconico.UserAuth +{ + public interface ICookieInfo + { + /// + /// UserSession + /// + string UserSession { get; set; } + + /// + /// UserSessionSecure + /// + string UserSessionSecure { get; set; } + } + + public class CookieInfo : ICookieInfo + { + public CookieInfo(ICookieStore store,string userSession,string userSessionSecure) + { + this._store = store; + this._userSession = userSession; + this._userSessionSecure = userSessionSecure; + } + + private string _userSession = string.Empty; + + private string _userSessionSecure = string.Empty; + + private readonly ICookieStore _store; + + public string UserSession + { + get => this._userSession; + set + { + this._userSession = value; + if (this.CanUpdate()) + { + this._store.Update(this); + } + } + } + + public string UserSessionSecure + { + get => this._userSessionSecure; + set + { + this._userSessionSecure = value; + if (this.CanUpdate()) + { + this._store.Update(this); + } + } + } + + private bool CanUpdate() + { + return !this.UserSessionSecure.IsNullOrEmpty()&&!this.UserSession.IsNullOrEmpty(); + } + } +} diff --git a/Niconicome/Models/Domain/Niconico/Video/Infomations/DmcInfo.cs b/Niconicome/Models/Domain/Niconico/Video/Infomations/DmcInfo.cs index 0bacfe36..70497971 100644 --- a/Niconicome/Models/Domain/Niconico/Video/Infomations/DmcInfo.cs +++ b/Niconicome/Models/Domain/Niconico/Video/Infomations/DmcInfo.cs @@ -31,10 +31,10 @@ public interface IDmcInfo bool IsPremium { get; set; } bool IsPeakTime { get; set; } bool IsEconomy { get; } + bool IsDMS { get; } DateTime UploadedOn { get; set; } DateTime DownloadStartedOn { get; set; } IThumbInfo ThumbInfo { get; set; } - ISessionInfo SessionInfo { get; } IReadOnlyCollection CommentTargets { get; } } @@ -104,6 +104,8 @@ public class DmcInfo : IDmcInfo public bool IsEconomy => !this.IsPremium && this.IsPeakTime && this.ViewCount >= Const::NetConstant.EconomyAvoidableViewCount; + public bool IsDMS { get; set; } + /// /// 投稿日時 /// @@ -120,11 +122,6 @@ public class DmcInfo : IDmcInfo /// public IThumbInfo ThumbInfo { get; set; } = new ThumbInfo(); - /// - /// セッション情報 - /// - public ISessionInfo SessionInfo { get; init; } = new SessionInfo(); - /// /// コメントターゲット /// diff --git a/Niconicome/Models/Domain/Niconico/Video/Infomations/DomainVideoInfo.cs b/Niconicome/Models/Domain/Niconico/Video/Infomations/DomainVideoInfo.cs index 8ed79dba..d1488f8b 100644 --- a/Niconicome/Models/Domain/Niconico/Video/Infomations/DomainVideoInfo.cs +++ b/Niconicome/Models/Domain/Niconico/Video/Infomations/DomainVideoInfo.cs @@ -114,36 +114,6 @@ public IDmcInfo DmcInfo if (this.cachedDmcInfo is null) { dynamic rawhumb = this.RawDmcInfo.ThumbInfo; - dynamic rawSesison = this.RawDmcInfo.SessionInfo; - - ISessionInfo sessionInfo; - - if (this.RawDmcInfo.IsDownloadable) - { - - sessionInfo = new SessionInfo() - { - RecipeId = rawSesison.RecipeId, - ContentId = rawSesison.ContentId, - HeartbeatLifetime = rawSesison.HeartbeatLifetime, - Token = rawSesison.Token, - Signature = rawSesison.Signature, - AuthType = rawSesison.AuthType, - ContentKeyTimeout = rawSesison.ContentKeyTimeout, - ServiceUserId = rawSesison.ServiceUserId, - PlayerId = rawSesison.PlayerId, - TransferPriset = rawSesison.TransferPriset, - Priority = rawSesison.Priority, - KeyURL = rawSesison.KeyURL is Undefined ? string.Empty : rawSesison.KeyURL, - EncryptedKey = rawSesison.EncryptedKey is Undefined ? string.Empty : rawSesison.EncryptedKey, - }; - sessionInfo.Videos.AddRange(JsUtils.ToClrArray(rawSesison.Videos)); - sessionInfo.Audios.AddRange(JsUtils.ToClrArray(rawSesison.Audios)); - } - else - { - sessionInfo = new SessionInfo(); - } List tags = JsUtils.ToClrArray(this.RawDmcInfo.Tags); @@ -195,7 +165,6 @@ public IDmcInfo DmcInfo UploadedOn = JsUtils.ToLocalDateTime(this.RawDmcInfo.UploadedOn), DownloadStartedOn = JsUtils.ToLocalDateTime(this.RawDmcInfo.DownloadStartedOn), ThumbInfo = new ThumbInfo(rawhumb.large, rawhumb.middle, rawhumb.normal, rawhumb.player), - SessionInfo = sessionInfo, IsPremium = this.RawDmcInfo.IsPremium, IsPeakTime = this.RawDmcInfo.IsPeakTime, Tags = clrTags, @@ -203,6 +172,7 @@ public IDmcInfo DmcInfo Threadkey = this.RawDmcInfo.Threadkey, CommentLanguage = this.RawDmcInfo.CommentLanguage, CommentTargets = clrTargets.AsReadOnly(), + IsDMS = this.RawDmcInfo.IsDMS, }; diff --git a/Niconicome/Models/Domain/Niconico/Watch/DmcDataHandler.cs b/Niconicome/Models/Domain/Niconico/Watch/DmcDataHandler.cs deleted file mode 100644 index efbb452a..00000000 --- a/Niconicome/Models/Domain/Niconico/Watch/DmcDataHandler.cs +++ /dev/null @@ -1,180 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net.Http; -using System.Threading.Tasks; -using Niconicome.Extensions.System; -using Niconicome.Models.Domain.Niconico.Net.Json; -using Niconicome.Models.Domain.Niconico.Video.Infomations; -using DmcRequest = Niconicome.Models.Domain.Niconico.Net.Json.WatchPage.DMC.Request; -using DmcResponse = Niconicome.Models.Domain.Niconico.Net.Json.WatchPage.DMC.Response; - -namespace Niconicome.Models.Domain.Niconico.Watch -{ - public interface IDmcDataHandler - { - DmcRequest::DmcPostData GetPostData(IDomainVideoInfo videoInfo); - Task GetSessionInfoAsync(DmcRequest::DmcPostData dmcPostData); - Task GetSessionInfoAsync(IDomainVideoInfo videoinfo); - } - - /// - /// DMCサーバーとの通信を管理する - /// - public class DmcDataHandler : IDmcDataHandler - { - public DmcDataHandler(INicoHttp http) - { - this.http = http; - } - - /// - /// httpクライアント - /// - private readonly INicoHttp http; - - public DmcRequest::DmcPostData GetPostData(IDomainVideoInfo videoInfo) - { - var data = DmcRequest::DmcPostData.GetInstance(); - var sessionIfnfo = videoInfo.DmcInfo.SessionInfo; - - //nullチェック - var nullList = new List(); - var type = sessionIfnfo.GetType(); - var properties = type.GetProperties(); - foreach (var prop in properties) - { - string name = prop.Name; - object? value = prop.GetValue(sessionIfnfo); - if (value is null) - { - nullList.Add(name); - } - } - - if (nullList.Count > 0) - { - throw new InvalidOperationException($"DMCサーバーへのPOSTに必要なデータがnullです。(一覧: {string.Join(',', nullList)})"); - } - - data.Session.Recipe_id = sessionIfnfo.RecipeId; - data.Session.Content_id = sessionIfnfo.ContentId; - data.Session.Content_type = "movie"; - data.Session.Content_src_id_sets.Add(this.GetContentSrcIdSets(sessionIfnfo)); - data.Session.Timing_constraint = "unlimited"; - data.Session.Keep_method.Heartbeat.Lifetime = sessionIfnfo.HeartbeatLifetime; - data.Session.Protocol.Name = "http"; - data.Session.Protocol.Parameters.Http_parameters.Parameters.Hls_parameters.Use_ssl = "yes"; - data.Session.Protocol.Parameters.Http_parameters.Parameters.Hls_parameters.Use_well_known_port = "yes"; - data.Session.Protocol.Parameters.Http_parameters.Parameters.Hls_parameters.Transfer_preset = sessionIfnfo.TransferPriset; - data.Session.Protocol.Parameters.Http_parameters.Parameters.Hls_parameters.Segment_duration = 6000; - data.Session.Session_operation_auth.Session_operation_auth_by_signature.Signature = sessionIfnfo.Signature; - data.Session.Session_operation_auth.Session_operation_auth_by_signature.Token = sessionIfnfo.Token; - data.Session.Content_auth.Auth_type = sessionIfnfo.AuthType; - data.Session.Content_auth.Service_id = "nicovideo"; - data.Session.Content_auth.Service_user_id = sessionIfnfo.ServiceUserId; - data.Session.Content_auth.Content_key_timeout = sessionIfnfo.ContentKeyTimeout; - data.Session.Client_info.Player_id = sessionIfnfo.PlayerId; - data.Session.Priority = sessionIfnfo.Priority; - - - return data; - } - - /// - /// セッション情報を取得する - /// - /// - /// - public async Task GetSessionInfoAsync(DmcRequest::DmcPostData dmcPostData) - { - string json = JsonParser.Serialize(dmcPostData); - - var res = await this.http.PostAsync(new Uri("https://api.dmc.nico/api/sessions?_format=json"), new StringContent(json)); - if (!res.IsSuccessStatusCode) - { - throw new HttpRequestException($"DMCサーバーへのPOSTに失敗しました。(status: {(int)res.StatusCode}, reason_phrase: {res.ReasonPhrase})"); - } - - string responseString = await res.Content.ReadAsStringAsync(); - - var deserialized = JsonParser.DeSerialize(responseString); - - if (deserialized.Data.Session.ContentUri.IsNullOrEmpty()) - { - throw new HttpRequestException("DMCサーバーからの情報取得に失敗しました。(Data.Session.ContentUriがnullまたは空白です。)"); - } - else if (deserialized.Data.Session.Id.IsNullOrEmpty()) - { - throw new HttpRequestException("DMCサーバーからの情報取得に失敗しました。(Data.Idがnullまたは空白です。)"); - } - - return new WatchSessionInfo() - { - DmcResponseJsonData = JsonParser.Serialize(deserialized.Data), - ContentUrl = deserialized.Data.Session.ContentUri, - SessionId = deserialized.Data.Session.Id, - }; - } - - /// - /// セッション情報を取得する - /// - /// - /// - public async Task GetSessionInfoAsync(IDomainVideoInfo videoinfo) - { - var postData = this.GetPostData(videoinfo); - return await this.GetSessionInfoAsync(postData); - } - - #region private - - - /// - /// Content_Src_Id_Setsを構成する - /// - /// - /// - private DmcRequest::Content_Src_Id_Sets GetContentSrcIdSets(ISessionInfo sessionInfo) - { - - sessionInfo.Videos.Sort((a, b) => - { - if (a.EndsWith("_low")) return -1; - if (b.EndsWith("_low")) return 1; - return string.Compare(a, b); - }); - var videoSrc = sessionInfo.Videos.Select((value, index) => new { value, index }).ToList(); - string audio = sessionInfo.Audios[0]; - var sets = new DmcRequest::Content_Src_Id_Sets(); - - foreach (var video in videoSrc) - { - var idsData = new DmcRequest::Content_Src_Ids(); - idsData.Src_id_to_mux.Audio_src_ids.Add(audio); - int videosCount = videoSrc.Count - video.index; - - foreach (var i in Enumerable.Range(0, videosCount)) - { - idsData.Src_id_to_mux.Video_src_ids.Add(videoSrc[i].value); - } - - //idsData.Src_id_to_mux.Video_src_ids = idsData.Src_id_to_mux.Video_src_ids.OrderByDescending(s => s).ToList(); - idsData.Src_id_to_mux.Video_src_ids.Sort((a, b) => - { - if (a.EndsWith("_low")) return 1; - if (b.EndsWith("_low")) return -1; - return string.Compare(b, a); - }); - - sets.Content_src_ids.Add(idsData); - - } - - return sets; - } - #endregion - } - -} diff --git a/Niconicome/Models/Domain/Niconico/Watch/WatchInfohandler.cs b/Niconicome/Models/Domain/Niconico/Watch/WatchInfohandler.cs index e8b13c3c..dca19d9e 100644 --- a/Niconicome/Models/Domain/Niconico/Watch/WatchInfohandler.cs +++ b/Niconicome/Models/Domain/Niconico/Watch/WatchInfohandler.cs @@ -52,7 +52,7 @@ public WatchInfohandler(INicoHttp http, ILogger logger, IHooksManager hooksManag public async Task> GetVideoInfoAsync(string id) { string source; - Uri url = NiconicoContext.Context.GetPageUri(id); + Uri url = new Uri("https://nicovideo.jp"); try { diff --git a/Niconicome/Models/Domain/Niconico/Watch/WatchSession.cs b/Niconicome/Models/Domain/Niconico/Watch/WatchSession.cs index 020fbe16..1f6e49fd 100644 --- a/Niconicome/Models/Domain/Niconico/Watch/WatchSession.cs +++ b/Niconicome/Models/Domain/Niconico/Watch/WatchSession.cs @@ -40,12 +40,11 @@ public enum WatchSessionState /// public class WatchSession : IWatchSession { - public WatchSession(IWatchInfohandler watchInfo, INicoHttp http, Utils::ILogger logger, IDmcDataHandler dmchandler, IWatchPlaylisthandler playlisthandler, IHooksManager hooksManager) + public WatchSession(IWatchInfohandler watchInfo, INicoHttp http, Utils::ILogger logger, IWatchPlaylisthandler playlisthandler, IHooksManager hooksManager) { this.watchInfo = watchInfo; this.http = http; this.logger = logger; - this.dmchandler = dmchandler; this.playlisthandler = playlisthandler; this.hooksManager = hooksManager; } @@ -59,8 +58,6 @@ public WatchSession(IWatchInfohandler watchInfo, INicoHttp http, Utils::ILogger private readonly Utils::ILogger logger; - private readonly IDmcDataHandler dmchandler; - private readonly IWatchInfohandler watchInfo; private readonly IWatchPlaylisthandler playlisthandler; @@ -164,17 +161,16 @@ public async Task EnsureSessionAsync(string nicoId) return; } - IAttemptResult result; - if (this.hooksManager.IsRegistered(HookType.SessionEnsuring)) - { - result = await this.EnsureSessionWithAddonAsync(this.Video); - } - else + if (!this.hooksManager.IsRegistered(HookType.SessionEnsuring)) { - result = await this.EnsureSessionDefaultAsync(this.Video); + this.State = WatchSessionState.SessionEnsuringFailure; + return; } + IAttemptResult result = await this.EnsureSessionWithAddonAsync(this.Video); + + if (!result.IsSucceeded || result.Data is null) { this.State = WatchSessionState.SessionEnsuringFailure; @@ -225,29 +221,6 @@ public void Dispose() #region private - /// - /// セッションを確立する - /// - /// - /// - private async Task> EnsureSessionDefaultAsync(IDomainVideoInfo video) - { - IWatchSessionInfo sessionInfo; - - try - { - sessionInfo = await this.dmchandler.GetSessionInfoAsync(video); - } - catch (Exception e) - { - this.logger.Error("セッションの確立に失敗しました。", e); - return new AttemptResult() { Message = "セッションの確立に失敗しました。", Exception = e }; - } - - return new AttemptResult() { IsSucceeded = true, Data = sessionInfo }; - - } - /// /// アドオンでセッションを確立する /// diff --git a/Niconicome/Models/Domain/Niconico/Watch/WatchSessionInfo.cs b/Niconicome/Models/Domain/Niconico/Watch/WatchSessionInfo.cs index 412a7455..d309db1a 100644 --- a/Niconicome/Models/Domain/Niconico/Watch/WatchSessionInfo.cs +++ b/Niconicome/Models/Domain/Niconico/Watch/WatchSessionInfo.cs @@ -8,19 +8,37 @@ namespace Niconicome.Models.Domain.Niconico.Watch { public interface IWatchSessionInfo { + /// + /// 旧サーバーハートビート + /// string DmcResponseJsonData { get; } + + /// + /// URL + /// string ContentUrl { get; } + + /// + /// SessionID + /// string SessionId { get; } + /// + /// Domandサーバーフラグ + /// + bool IsDMS { get; } + } - /// - /// セッション情報 - /// public class WatchSessionInfo : IWatchSessionInfo { public string DmcResponseJsonData { get; set; } = string.Empty; + public string ContentUrl { get; set; } = string.Empty; + public string SessionId { get; set; } = string.Empty; + + public bool IsDMS { get; set; } } + } diff --git a/Niconicome/Models/Domain/Playlist/VideoInfo.cs b/Niconicome/Models/Domain/Playlist/VideoInfo.cs index c0299eab..21f6a5d0 100644 --- a/Niconicome/Models/Domain/Playlist/VideoInfo.cs +++ b/Niconicome/Models/Domain/Playlist/VideoInfo.cs @@ -145,6 +145,11 @@ public interface IVideoInfo : IUpdatable /// bool IsEconomy { get; set; } + /// + /// 新サーバーかどうか + /// + bool IsDMS { get; } + /// /// タグを追加 /// @@ -433,6 +438,8 @@ public bool IsEconomy } } + public bool IsDMS => !string.IsNullOrEmpty(this.FilePath) && this.FilePath.EndsWith("stream.json"); + #endregion #region Method diff --git a/Niconicome/Models/Domain/Utils/BackgroundTask/BackgroundTask.cs b/Niconicome/Models/Domain/Utils/BackgroundTask/BackgroundTask.cs new file mode 100644 index 00000000..c774d03e --- /dev/null +++ b/Niconicome/Models/Domain/Utils/BackgroundTask/BackgroundTask.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Niconicome.Models.Utils.Reactive; + +namespace Niconicome.Models.Domain.Utils.BackgroundTask +{ + public record BackgroundTask(IBindableProperty IsDone, string TaskID); +} diff --git a/Niconicome/Models/Domain/Utils/BackgroundTask/BackgroundTaskManager.cs b/Niconicome/Models/Domain/Utils/BackgroundTask/BackgroundTaskManager.cs new file mode 100644 index 00000000..70ea1bfd --- /dev/null +++ b/Niconicome/Models/Domain/Utils/BackgroundTask/BackgroundTaskManager.cs @@ -0,0 +1,190 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Timers; +using Niconicome.Models.Utils.Reactive; + +namespace Niconicome.Models.Domain.Utils.BackgroundTask +{ + public interface IBackgroundTaskManager + { + /// + /// タスクを追加 + /// + /// + /// + /// + BackgroundTask AddTask(Action action, bool highPriority = false); + + /// + /// 時間指定でタスクを追加 + /// + /// + /// + /// + BackgroundTask AddTimerTask(Action action, DateTime when); + + /// + /// タスクをキャンセル + /// + /// + void CancelTask(string taskID); + } + + public class BackgroundTaskManager : IBackgroundTaskManager + { + public BackgroundTask AddTask(Action action, bool highPriority = false) + { + var task = new BackgroundTask(new BindableProperty(false), Utils.GetRandomString(5)); + if (highPriority) + { + this.EnqueueHighPriorityTask(new TaskObject(action, task)); + } else + { + this.EnqueueTask(new TaskObject(action, task)); + } + + if (!this._isRunning) + { + _= this.WorkRoop(); + } + return task; + } + + public BackgroundTask AddTimerTask(Action action, DateTime when) + { + var task = new BackgroundTask(new BindableProperty(false), Utils.GetRandomString(5)); + var timer = new Timer((when - DateTime.Now).TotalMilliseconds); + timer.AutoReset = false; + timer.Elapsed += (sender, e) => + { + this.EnqueueTask(new TaskObject(action, task)); + if (!this._isRunning) + { + _ = this.WorkRoop(); + } + this._timers.Remove(task.TaskID); + }; + timer.Start(); + this._timers.Add(task.TaskID, timer); + return task; + } + + public void CancelTask(string taskID) + { + this._tasks.RemoveAll(_tasks => _tasks.taskInfo.TaskID == taskID); + if (this._timers.ContainsKey(taskID)) + { + var timer = this._timers[taskID]; + timer.Stop(); + timer.Dispose(); + this._timers.Remove(taskID); + } + } + + /// + /// キュー + /// + private readonly List _tasks = new(); + + /// + /// タイマー + /// + private readonly Dictionary _timers = new(); + + private bool _isRunning = false; + + #region private + + /// + /// タスク処理 + /// + private async Task WorkRoop() + { + this._isRunning = true; + + if (this._tasks.Count == 0) + { + this._isRunning = false; + return; + } + + var task = this.DequeueTask(); + + await Task.Run(() => + { + try + { + task.action(); + } + catch + { } + }); + + task.taskInfo.IsDone.Value = true; + + if (this._tasks.Count > 0) + { + _ = this.WorkRoop(); + return; + } + + this._isRunning = false; + } + + /// + /// タスクを追加 + /// + /// + private void EnqueueTask(TaskObject task) + { + this._tasks.Add(task); + } + + /// + /// 優先度の高いタスクを追加 + /// + /// + private void EnqueueHighPriorityTask(TaskObject task) + { + this._tasks.Insert(0, task); + } + + /// + /// タスクを取得 + /// + /// + private TaskObject DequeueTask() + { + if (this._tasks.Count == 0) + { + throw new InvalidOperationException(); + } + + var task = this._tasks.First(); + this._tasks.RemoveAt(0); + return task; + + } + + /// + /// タスクIDを取得 + /// + /// + private string GetTaskID() + { + return $"{Utils.GetRandomString(5)}-{DateTime.Now.ToString("HHmmss")}"; + } + + #endregion + + /// + /// タスクオブジェクト + /// + /// + /// + private record TaskObject(Action action, BackgroundTask taskInfo); + } +} diff --git a/Niconicome/Models/Domain/Utils/DIFactory.cs b/Niconicome/Models/Domain/Utils/DIFactory.cs index adac6da6..7b8dba93 100644 --- a/Niconicome/Models/Domain/Utils/DIFactory.cs +++ b/Niconicome/Models/Domain/Utils/DIFactory.cs @@ -1,5 +1,4 @@ -using System.Net.Http; -using System.Net.Security; +using System.Net; using Microsoft.Extensions.DependencyInjection; using AddonAPI = Niconicome.Models.Local.Addon.API; using AddonsCoreV2 = Niconicome.Models.Domain.Local.Addons.Core.V2; @@ -9,6 +8,7 @@ using Auth = Niconicome.Models.Auth; using Backup = Niconicome.Models.Domain.Local.DataBackup; using Channel = Niconicome.Models.Domain.Niconico.Remote.Channel; +using CommentAPIV1 = Niconicome.Models.Domain.Local.Server.API.Comment.V1; using CommentConverter = Niconicome.Models.Domain.Niconico.Download.Comment.V2.Core.Converter; using CommentFetch = Niconicome.Models.Domain.Niconico.Download.Comment.V2.Fetch; using CommentIntegrate = Niconicome.Models.Domain.Niconico.Download.Comment.V2.Integrate; @@ -17,11 +17,14 @@ using DataBase = Niconicome.Models.Domain.Local; using DB = Niconicome.Models.Infrastructure.Database; using DLActions = Niconicome.Models.Network.Download.Actions; -using DlComment = Niconicome.Models.Domain.Niconico.Download.Comment; using DlDescription = Niconicome.Models.Domain.Niconico.Download.Description; +using DLGeneral = Niconicome.Models.Domain.Niconico.Download.General; using DlIchiba = Niconicome.Models.Domain.Niconico.Download.Ichiba; +using DLTaskVM = Niconicome.ViewModels.Mainpage.Subwindows.DownloadTask; using DlThumb = Niconicome.Models.Domain.Niconico.Download.Thumbnail; using DlVideo = Niconicome.Models.Domain.Niconico.Download.Video; +using DlVideoV2 = Niconicome.Models.Domain.Niconico.Download.Video.V2; +using DlVideoV3 = Niconicome.Models.Domain.Niconico.Download.Video.V3; using Dmc = Niconicome.Models.Domain.Niconico.Dmc; using DomainExt = Niconicome.Models.Domain.Local.External; using DomainNet = Niconicome.Models.Domain.Network; @@ -46,38 +49,44 @@ using Mylist = Niconicome.Models.Domain.Niconico.Remote.Mylist; using Net = Niconicome.Models.Network; using NetworkVideo = Niconicome.Models.Network.Video; +using NGAPIV1 = Niconicome.Models.Domain.Local.Server.API.NG.V1; +using NicoImport = Niconicome.Models.Domain.Local.DataBackup.Import.Niconicome; using Niconico = Niconicome.Models.Domain.Niconico; using OS = Niconicome.Models.Local.OS; using Playlist = Niconicome.Models.Playlist; using PlaylistPlaylist = Niconicome.Models.Playlist.Playlist; using PlaylistV2 = Niconicome.Models.Playlist.V2; +using RegacyHLSAPIV1 = Niconicome.Models.Domain.Local.Server.API.RegacyHLS.V1; using Register = Niconicome.Models.Network.Register; using RemoteV2 = Niconicome.Models.Domain.Niconico.Remote.V2; +using ResourceAPIV1 = Niconicome.Models.Domain.Local.Server.API.Resource.V1; using Restore = Niconicome.Models.Local.Restore; using Resume = Niconicome.Models.Domain.Niconico.Download.Video.Resume; using Search = Niconicome.Models.Domain.Niconico.Remote.Search; using Series = Niconicome.Models.Domain.Niconico.Remote.Series; using Server = Niconicome.Models.Domain.Local.Server; using Settings = Niconicome.Models.Local.Settings; +using SettingsVM = Niconicome.ViewModels.Setting.V2.Page; +using Software = Niconicome.Models.Domain.Local.External.Software; using SQlite = Niconicome.Models.Domain.Local.SQLite; using State = Niconicome.Models.Local.State; using Store = Niconicome.Models.Domain.Local.Store; using Style = Niconicome.Models.Domain.Local.Style; +using Tab = Niconicome.Models.Local.State.Tab.V1; using TabsVM = Niconicome.ViewModels.Mainpage.Tabs; using Timer = Niconicome.Models.Local.Timer; using Utils = Niconicome.Models.Utils; using UVideo = Niconicome.Models.Domain.Niconico.Video; +using VideoInfoAPIV1 = Niconicome.Models.Domain.Local.Server.API.VideoInfo.V1; using VList = Niconicome.Models.Playlist.VideoList; using VM = Niconicome.ViewModels; using Watch = Niconicome.Models.Network.Watch; -using Software = Niconicome.Models.Domain.Local.External.Software; -using NicoImport = Niconicome.Models.Domain.Local.DataBackup.Import.Niconicome; -using SettingsVM = Niconicome.ViewModels.Setting.V2.Page; +using WatchAPIv1 = Niconicome.Models.Domain.Local.Server.API.Watch.V1; using XenoImport = Niconicome.Models.Domain.Local.DataBackup.Import.Xeno; -using DlVideoV2 = Niconicome.Models.Domain.Niconico.Download.Video.V2; -using DLGeneral = Niconicome.Models.Domain.Niconico.Download.General; -using DLTaskVM = Niconicome.ViewModels.Mainpage.Subwindows.DownloadTask; - +using BackgroundTask = Niconicome.Models.Domain.Utils.BackgroundTask; +using Modify = Niconicome.Models.Network.Download.Modification; +using UserAuth = Niconicome.Models.Domain.Niconico.UserAuth; +using Cookie = Niconicome.Models.Auth.Cookie; namespace Niconicome.Models.Domain.Utils { @@ -89,23 +98,14 @@ private static ServiceProvider GetProvider() services.AddWpfBlazorWebView(); services.AddBlazorWebViewDeveloperTools(); services.AddHttpClient() - .ConfigureHttpMessageHandlerBuilder(builder => + .ConfigurePrimaryHttpMessageHandler(provider => { - var shandler = builder.Services.GetRequiredService(); + var shandler = provider.GetRequiredService(); var skip = shandler.GetBoolSetting(Settings::SettingsEnum.SkipSSLVerification); - if (builder.PrimaryHandler is HttpClientHandler handler) - { - handler.CookieContainer = builder.Services.GetRequiredService().CookieContainer; - handler.UseCookies = true; - handler.ServerCertificateCustomValidationCallback = (sender, cert, chain, sslPolicyErrors) => - { - if (skip) return true; - return sslPolicyErrors == SslPolicyErrors.None; - }; - } + CookieContainer container = provider.GetRequiredService().CookieContainer; + return new Niconico::NicoHttpClientHandler(container, skip); }); services.AddSingleton(); - services.AddTransient(); services.AddSingleton(); services.AddTransient(); services.AddTransient(); @@ -131,7 +131,6 @@ private static ServiceProvider GetProvider() services.AddSingleton(); services.AddSingleton(); services.AddTransient(); - services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); @@ -180,8 +179,10 @@ private static ServiceProvider GetProvider() services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); @@ -225,6 +226,7 @@ private static ServiceProvider GetProvider() services.AddSingleton(); services.AddTransient(); services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddTransient(); services.AddTransient(); @@ -241,10 +243,8 @@ private static ServiceProvider GetProvider() services.AddSingleton(); services.AddTransient(); services.AddTransient(); - services.AddTransient(); services.AddTransient(); services.AddSingleton(); - services.AddSingleton(); services.AddTransient(); services.AddTransient(); services.AddTransient(); @@ -300,7 +300,7 @@ private static ServiceProvider GetProvider() services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); - services.AddTransient(); + services.AddSingleton(); services.AddTransient(); services.AddTransient(); services.AddTransient(); @@ -341,6 +341,7 @@ private static ServiceProvider GetProvider() services.AddSingleton(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); @@ -371,11 +372,11 @@ private static ServiceProvider GetProvider() services.AddTransient(); services.AddTransient(); services.AddTransient(); - services.AddTransient(); + services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); - services.AddTransient(); + services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); @@ -388,6 +389,44 @@ private static ServiceProvider GetProvider() services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddSingleton(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddSingleton(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddSingleton(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddSingleton(); return services.BuildServiceProvider(); } diff --git a/Niconicome/Models/Domain/Utils/Error/ErrorHandler.cs b/Niconicome/Models/Domain/Utils/Error/ErrorHandler.cs index c006c541..9e1fcbb2 100644 --- a/Niconicome/Models/Domain/Utils/Error/ErrorHandler.cs +++ b/Niconicome/Models/Domain/Utils/Error/ErrorHandler.cs @@ -19,7 +19,7 @@ public interface IErrorHandler /// /// /// - void HandleError(T value, params object[]? items) where T : struct, Enum; + string HandleError(T value, params object[]? items) where T : struct, Enum; /// /// 例外情報を伴うエラー処理を行う @@ -28,7 +28,7 @@ public interface IErrorHandler /// /// /// - void HandleError(T value, Exception ex, params object[]? items) where T : struct, Enum; + string HandleError(T value, Exception ex, params object[]? items) where T : struct, Enum; /// /// 例外情報を文字列で取得する @@ -65,7 +65,7 @@ public ErrorHandler(INiconicomeLogger logger) #region Method - public void HandleError(T value, params object[]? items) where T : struct, Enum + public string HandleError(T value, params object[]? items) where T : struct, Enum { if (this.TryGetAttr(value, out ErrorEnumAttribute? attr)) { @@ -73,10 +73,16 @@ public void HandleError(T value, params object[]? items) where T : struct, En var message = $"[{errorCode}] {this.GetErrorMessage(attr, items ?? new object[0])}"; this.WriteLog(attr.ErrorLevel, message); + + return this.GetMessageForResult(value, items); + } + else + { + return string.Empty; } } - public void HandleError(T value, Exception ex, params object[]? items) where T : struct, Enum + public string HandleError(T value, Exception ex, params object[]? items) where T : struct, Enum { if (this.TryGetAttr(value, out ErrorEnumAttribute? attr)) { @@ -85,6 +91,12 @@ public void HandleError(T value, Exception ex, params object[]? items) where var message = $"[{errorCode}] {this.GetErrorMessage(attr, items ?? new object[0])}{Environment.NewLine}{exMessage}"; this.WriteLog(attr.ErrorLevel, message); + + return this.GetMessageForResult(value, ex, items); + } + else + { + return string.Empty; } } diff --git a/Niconicome/Models/Domain/Utils/Error/ErrorType.cs b/Niconicome/Models/Domain/Utils/Error/ErrorType.cs index 5166f7c8..401f2467 100644 --- a/Niconicome/Models/Domain/Utils/Error/ErrorType.cs +++ b/Niconicome/Models/Domain/Utils/Error/ErrorType.cs @@ -41,6 +41,15 @@ using Niconicome.Models.Network.Download.DLTask.Error; using Niconicome.Models.Network.Video.Error; using Niconicome.Models.Playlist.V2.Manager.Error; +using VDL3 = Niconicome.Models.Domain.Niconico.Download.Video.V3.Error; +using WatchAPIV1 = Niconicome.Models.Domain.Local.Server.API.Watch.V1.Error; +using CommentAPIV1 = Niconicome.Models.Domain.Local.Server.API.Comment.V1.Error; +using HLSAPIV1 = Niconicome.Models.Domain.Local.Server.API.RegacyHLS.V1.Error; +using ResourceAPIV1 = Niconicome.Models.Domain.Local.Server.API.Resource.V1.Error; +using VideoInfoAPIV1 = Niconicome.Models.Domain.Local.Server.API.VideoInfo.V1.Error; +using PostDL = Niconicome.Models.Network.Download.Actions.V2; +using Auth = Niconicome.Models.Auth.Error; +using Niconico = Niconicome.Models.Domain.Niconico; namespace Niconicome.Models.Domain.Utils.Error { @@ -108,6 +117,25 @@ public static class ErrorTypes { 55, typeof(DownloadManagerError) }, { 56, typeof(NaudioHandlerError) }, { 57, typeof(LocalFileRemoverError) }, + { 58, typeof(VDL3.StreamParserError) }, + { 59, typeof(VDL3.SegmentDownloaderError) }, + { 60, typeof(VDL3.SegmentDirectoryHandlerError) }, + { 61, typeof(VDL3.SegmentWriterError) }, + { 62, typeof(VDL3.WatchSessionError) }, + { 63, typeof(VDL3.KeyDownlaoderError) }, + { 64, typeof(VDL3.StreamJsonHandlerError) }, + { 65, typeof(WatchAPIV1.WatchHandlerError) }, + { 66, typeof(WatchAPIV1.DecryptorError) }, + { 67, typeof(CommentAPIV1::CommentRequestHandlerError) }, + { 68, typeof(CommentAPIV1::CommentRetreiverError) }, + { 69, typeof(HLSAPIV1::HLSManagerError) }, + { 70, typeof(HLSAPIV1::RegacyHLSHandlerError) }, + { 71, typeof(ResourceAPIV1::ResourceHandlerError) }, + { 72, typeof(VideoInfoAPIV1::VideoInfoHandlerError) }, + { 73, typeof(PostDL::PostDownloadActionsManagerError) }, + { 74, typeof(Auth::ChromeSharedLoginError) }, + { 75, typeof(CookieDBHandlerError) }, + { 76, typeof(Niconico::NiconicoContextError) }, }; } } diff --git a/Niconicome/Models/Domain/Utils/PathOrganizer.cs b/Niconicome/Models/Domain/Utils/PathOrganizer.cs index 3fc56249..a3c2a76e 100644 --- a/Niconicome/Models/Domain/Utils/PathOrganizer.cs +++ b/Niconicome/Models/Domain/Utils/PathOrganizer.cs @@ -37,5 +37,6 @@ public string GetFilePath(string format, IDmcInfo dmcInfo, string extension, str return filePath; } + } } diff --git a/Niconicome/Models/Domain/Utils/Utitls.cs b/Niconicome/Models/Domain/Utils/Utitls.cs index 38c86faf..d65393ca 100644 --- a/Niconicome/Models/Domain/Utils/Utitls.cs +++ b/Niconicome/Models/Domain/Utils/Utitls.cs @@ -39,6 +39,25 @@ public static Brush ConvertToBrush(string colorString) var color = ColorConverter.ConvertFromString(colorString); return new SolidColorBrush((Color)color); } + + /// + /// 指定長のランダムな文字列を取得する + /// + /// + /// + public static string GetRandomString(int length) + { + var random = new Random(); + var chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + return new string(Enumerable.Repeat(chars, length).Select(s => s[random.Next(s.Length)]).ToArray()); + } + + public static string GetStrongRandomString(int length) + { + var random = new Random(); + var chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!#$%&'()*+,-./:;<=>?@[]^_`{|}~"; + return new string(Enumerable.Repeat(chars, length).Select(s => s[random.Next(s.Length)]).ToArray()); + } } } diff --git a/Niconicome/Models/Helper/Result/Generic/AttemptResult.cs b/Niconicome/Models/Helper/Result/Generic/AttemptResult.cs index 952599b1..e01b3074 100644 --- a/Niconicome/Models/Helper/Result/Generic/AttemptResult.cs +++ b/Niconicome/Models/Helper/Result/Generic/AttemptResult.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using Niconicome.Models.Domain.Utils.Error; namespace Niconicome.Models.Helper.Result diff --git a/Niconicome/Models/Infrastructure/Database/CookieDBHandler.cs b/Niconicome/Models/Infrastructure/Database/CookieDBHandler.cs new file mode 100644 index 00000000..faf0ec48 --- /dev/null +++ b/Niconicome/Models/Infrastructure/Database/CookieDBHandler.cs @@ -0,0 +1,245 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; +using Niconicome.Models.Domain.Local.Store.V2; +using Niconicome.Models.Domain.Niconico.UserAuth; +using Niconicome.Models.Domain.Utils.Error; +using Niconicome.Models.Helper.Result; +using Niconicome.Models.Infrastructure.Database.LiteDB; +using Types = Niconicome.Models.Domain.Local.Store.Types; +using LocalCnst = Niconicome.Models.Const.LocalConstant; +using Utl = Niconicome.Models.Domain.Utils; +using Err = Niconicome.Models.Infrastructure.Database.Error.CookieDBHandlerError; +using System.Diagnostics; +using Niconicome.Extensions.System; + +namespace Niconicome.Models.Infrastructure.Database +{ + public class CookieDBHandler : ICookieStore + { + public CookieDBHandler(ILiteDBHandler dataBase, IErrorHandler errorHandler) + { + this._dataBase = dataBase; + this._errorHandler = errorHandler; + } + + #region field + + private readonly ILiteDBHandler _dataBase; + + private readonly IErrorHandler _errorHandler; + + private readonly int AES_KEY_SIZE_BIT = 128; + + #endregion + + #region Method + + public IAttemptResult GetCookieInfo() + { + this.Initialize(); + + IAttemptResult> cookieResult = this._dataBase.GetAllRecords(TableNames.Cookie); + + if (!cookieResult.IsSucceeded || cookieResult.Data is null) return AttemptResult.Fail(cookieResult.Message); + + var cookie = cookieResult.Data.First(); + + if (cookie.UserSession.IsNullOrEmpty()) + { + var empty = new CookieInfo(this, string.Empty, string.Empty); + return AttemptResult.Succeeded(empty); + } + + Decrypted decrypted; + + try + { + decrypted = this.DecryptCookie(cookie); + } + catch (Exception ex) + { + this.DeleteCookieInfo(); + return AttemptResult.Fail(this._errorHandler.HandleError(Err.FailedToDecrypt, ex)); + } + + var info = new CookieInfo(this, decrypted.UserSession, decrypted.UserSessionSecure); + + return AttemptResult.Succeeded(info); + } + + + public IAttemptResult DeleteCookieInfo() + { + return this._dataBase.Clear(TableNames.Cookie); + } + + + public IAttemptResult Update(ICookieInfo cookieInfo) + { + if (!this.Exists()) return AttemptResult.Fail(this._errorHandler.HandleError(Err.CookieNotFound)); + if (cookieInfo.UserSession.IsNullOrEmpty() || cookieInfo.UserSessionSecure.IsNullOrEmpty()) return AttemptResult.Succeeded(); + + IAttemptResult> cookieResult = this._dataBase.GetAllRecords(TableNames.Cookie); + + if (!cookieResult.IsSucceeded || cookieResult.Data is null) return AttemptResult.Fail(cookieResult.Message); + + var original = cookieResult.Data.First(); + Types.Cookie encrypted; + + try + { + encrypted = this.EncryptCookie(cookieInfo.UserSession, cookieInfo.UserSessionSecure); + } + catch (Exception ex) + { + return AttemptResult.Fail(this._errorHandler.HandleError(Err.FailedToEncrypt, ex)); + } + + encrypted.Id = original.Id; + + return this._dataBase.Update(encrypted); + } + + public bool Exists() + { + IAttemptResult> cookie = this._dataBase.GetAllRecords(TableNames.Cookie); + if (!cookie.IsSucceeded || cookie.Data is null) return false; + + return cookie.Data.Count > 0; + } + + #endregion + + #region private + + /// + /// Cookieを復号 + /// + /// + /// + private Decrypted DecryptCookie(Types.Cookie cookie) + { + var appKey = Encoding.UTF8.GetBytes(LocalCnst.AppKey); + var key = ProtectedData.Unprotect(Convert.FromBase64String(cookie.Key), null, DataProtectionScope.CurrentUser); + key = Convert.FromBase64String(Encoding.UTF8.GetString(this.DecryptAES(key, appKey))); + + var userSession = this.DecryptAES(Convert.FromBase64String(cookie.UserSession), key); + var userSessionSecure = this.DecryptAES(Convert.FromBase64String(cookie.UserSessionSecure), key); + + return new Decrypted(Encoding.UTF8.GetString(userSession), Encoding.UTF8.GetString(userSessionSecure)); + } + + /// + /// Cookieを暗号化 + /// + /// + /// + /// + private Types.Cookie EncryptCookie(string userSession, string userSessionSecure) + { + var key = this.CreateKey(); + string keyB64 = Convert.ToBase64String(key); + var appKey = Encoding.UTF8.GetBytes(LocalCnst.AppKey); + + if (appKey.Length * 8 != this.AES_KEY_SIZE_BIT) throw new InvalidOperationException(); + + var userSessionChipher = this.EncryptAES(userSession, key); + var userSessionSecureChipher = this.EncryptAES(userSessionSecure, key); + + return new Types.Cookie() + { + Key = Convert.ToBase64String(ProtectedData.Protect(this.EncryptAES(keyB64,appKey), null, DataProtectionScope.CurrentUser)), + UserSession = Convert.ToBase64String(userSessionChipher), + UserSessionSecure = Convert.ToBase64String(userSessionSecureChipher), + }; + } + + /// + /// AESで暗号化された文字列を復号 + /// + /// + /// + /// + private byte[] DecryptAES(byte[] rawChipher, byte[] key) + { + byte[] iv = rawChipher[0..16]; + byte[] chipher = rawChipher[16..]; + + using (var aes = Aes.Create()) + { + using (var ms = new MemoryStream()) + using (var cs = new CryptoStream(ms, aes.CreateDecryptor(key, iv), CryptoStreamMode.Write)) + { + using (var writer = new BinaryWriter(cs)) + { + writer.Write(chipher); + } + return ms.ToArray(); + + } + } + + } + + /// + /// AESで暗号化 + /// + /// + /// + /// + private byte[] EncryptAES(string content, byte[] key) + { + using (var aes = Aes.Create()) + { + using (var ms = new MemoryStream()) + using (var cs = new CryptoStream(ms, aes.CreateEncryptor(key, aes.IV), CryptoStreamMode.Write)) + { + ms.Write(aes.IV); + + using (var writer = new StreamWriter(cs)) + { + writer.Write(content); + } + + byte[] chipher = ms.ToArray(); + + return chipher; + } + } + + } + + /// + /// キーを生成 + /// + /// + private byte[] CreateKey() + { + using var aes = Aes.Create(); + return aes.Key; + } + + /// + /// 初期化 + /// + private IAttemptResult Initialize() + { + IAttemptResult> cookie = this._dataBase.GetAllRecords(TableNames.Cookie); + if (!cookie.IsSucceeded || cookie.Data is null) return AttemptResult.Fail(cookie.Message); + + if (cookie.Data.Count > 0) return AttemptResult.Succeeded(); + + var app = new Types.Cookie(); + return this._dataBase.Insert(app); + } + + private record Decrypted(string UserSession, string UserSessionSecure); + + #endregion + } +} diff --git a/Niconicome/Models/Infrastructure/Database/Error/CookieDBHandlerError.cs b/Niconicome/Models/Infrastructure/Database/Error/CookieDBHandlerError.cs new file mode 100644 index 00000000..5438fd5d --- /dev/null +++ b/Niconicome/Models/Infrastructure/Database/Error/CookieDBHandlerError.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Niconicome.Models.Domain.Utils.Error; + +namespace Niconicome.Models.Infrastructure.Database.Error +{ + public enum CookieDBHandlerError + { + [ErrorEnum(ErrorLevel.Error,"Cookieが保存されていません。")] + CookieNotFound, + [ErrorEnum(ErrorLevel.Error,"Cookieの暗号化に失敗しました。")] + FailedToEncrypt, + [ErrorEnum(ErrorLevel.Error,"Cookieの復号に失敗しました。")] + FailedToDecrypt, + } +} diff --git a/Niconicome/Models/Infrastructure/Database/LiteDB/TableNames.cs b/Niconicome/Models/Infrastructure/Database/LiteDB/TableNames.cs index 5d25a475..443119c3 100644 --- a/Niconicome/Models/Infrastructure/Database/LiteDB/TableNames.cs +++ b/Niconicome/Models/Infrastructure/Database/LiteDB/TableNames.cs @@ -19,5 +19,7 @@ public static class TableNames public static string Tag = "v2_tag"; public static string VideoFile = "v2_videofile"; + + public static string Cookie = "cookie"; } } diff --git a/Niconicome/Models/Infrastructure/Database/Types/Cookie.cs b/Niconicome/Models/Infrastructure/Database/Types/Cookie.cs new file mode 100644 index 00000000..26af4022 --- /dev/null +++ b/Niconicome/Models/Infrastructure/Database/Types/Cookie.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Niconicome.Models.Infrastructure.Database.LiteDB; + +namespace Niconicome.Models.Infrastructure.Database.Types +{ + public class Cookie : IBaseStoreClass + { + public int Id { get; set; } + + public string TableName => TableNames.Cookie; + + /// + /// UserSession(暗号化) + /// + public string UserSession { get; set; } = string.Empty; + + /// + /// UserSessionSecure(暗号化) + /// + public string UserSessionSecure { get; set; } = string.Empty; + + /// + /// キー(暗号化) + /// + public string Key { get; set; } = string.Empty; + } +} diff --git a/Niconicome/Models/Infrastructure/IO/WindowsDirectoryIO.cs b/Niconicome/Models/Infrastructure/IO/WindowsDirectoryIO.cs index 32544b17..5330db0b 100644 --- a/Niconicome/Models/Infrastructure/IO/WindowsDirectoryIO.cs +++ b/Niconicome/Models/Infrastructure/IO/WindowsDirectoryIO.cs @@ -91,6 +91,22 @@ public IAttemptResult Delete(string path, bool recursive = true) return AttemptResult.Succeeded(); } + public IAttemptResult Move(string source, string destination, bool overwrite = true) + { + try + { + this.CopyDirectory(source, destination, overwrite); + } + catch (Exception ex) + { + this._errorHandler.HandleError(WindowsDirectoryIOError.FailedToMoveDirectory, ex, source, destination); + return AttemptResult.Fail(this._errorHandler.GetMessageForResult(WindowsDirectoryIOError.FailedToMoveDirectory, ex, source, destination)); + } + + return this.Delete(source); + } + + public bool Exists(string path) { @@ -99,5 +115,34 @@ public bool Exists(string path) #endregion + + private void CopyDirectory(string sourceDir, string destinationDir, bool overwrite) + { + // Get information about the source directory + var dir = new DirectoryInfo(sourceDir); + + // Cache directories before we start copying + DirectoryInfo[] dirs = dir.GetDirectories(); + + // Create the destination directory + if (!this.Exists(destinationDir)) + { + Directory.CreateDirectory(destinationDir); + } + + // Get the files in the source directory and copy to the destination directory + foreach (FileInfo file in dir.GetFiles()) + { + string targetFilePath = Path.Combine(destinationDir, file.Name); + file.CopyTo(targetFilePath,overwrite); + } + + // If recursive and copying subdirectories, recursively call this method + foreach (DirectoryInfo subDir in dirs) + { + string newDestinationDir = Path.Combine(destinationDir, subDir.Name); + CopyDirectory(subDir.FullName, newDestinationDir,overwrite); + } + } } } diff --git a/Niconicome/Models/Infrastructure/IO/WindowsDirectoryIOError.cs b/Niconicome/Models/Infrastructure/IO/WindowsDirectoryIOError.cs index 80042ab2..a5df5908 100644 --- a/Niconicome/Models/Infrastructure/IO/WindowsDirectoryIOError.cs +++ b/Niconicome/Models/Infrastructure/IO/WindowsDirectoryIOError.cs @@ -17,5 +17,7 @@ public enum WindowsDirectoryIOError FailedToGetDirectories, [ErrorEnum(ErrorLevel.Error, "ファイル一覧の取得に失敗しました。(path:{0})")] FailedToGetFiles, + [ErrorEnum(ErrorLevel.Error, "ディレクトリの移動に失敗しました。(source:{0},destination:{1})")] + FailedToMoveDirectory, } } diff --git a/Niconicome/Models/Infrastructure/IO/WindowsFileIO.cs b/Niconicome/Models/Infrastructure/IO/WindowsFileIO.cs index e75ac46b..8e982540 100644 --- a/Niconicome/Models/Infrastructure/IO/WindowsFileIO.cs +++ b/Niconicome/Models/Infrastructure/IO/WindowsFileIO.cs @@ -10,6 +10,7 @@ using Niconicome.Models.Domain.Utils; using Niconicome.Models.Helper.Result; using Error = Niconicome.Models.Domain.Utils.Error; +using Niconicome.Models.Domain.Local.Server.RequestHandler.TS; namespace Niconicome.Models.Infrastructure.IO { @@ -64,7 +65,6 @@ public IAttemptResult Write(string path, string content, Encoding? encoding = nu return AttemptResult.Succeeded(); } - public IAttemptResult Write(string path, byte[] content) { try @@ -81,6 +81,22 @@ public IAttemptResult Write(string path, byte[] content) return AttemptResult.Succeeded(); } + public IAttemptResult WriteToStream(string path, System.IO.Stream stream) + { + try + { + using var video = new FileStream(path, FileMode.Open, FileAccess.Read); + video.CopyTo(stream); + } + catch (Exception ex) + { + this._errorHandler.HandleError(WindowsFileIOError.FailedToWrite, ex, path); + return AttemptResult.Fail(this._errorHandler.GetMessageForResult(TSRequestHandlerError.FailedToOpenVodeo, ex)); + } + + return AttemptResult.Succeeded(); + } + public IAttemptResult Read(string path) { var content = string.Empty; @@ -99,6 +115,23 @@ public IAttemptResult Read(string path) return AttemptResult.Succeeded(content); } + public IAttemptResult ReadByte(string path) + { + byte[] content; + + try + { + content = File.ReadAllBytes(path); + } + catch (Exception ex) + { + this._errorHandler.HandleError(WindowsFileIOError.FailedToRead, ex, path); + return AttemptResult.Fail(this._errorHandler.GetMessageForResult(WindowsFileIOError.FailedToRead, ex, path)); + } + + return AttemptResult.Succeeded(content); + } + public void EnumerateFiles(string path, string searchPattern, Action enumAction, bool searchSubDirectory) { SearchOption option = searchSubDirectory ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly; diff --git a/Niconicome/Models/Infrastructure/Network/IPHandler.cs b/Niconicome/Models/Infrastructure/Network/IPHandler.cs new file mode 100644 index 00000000..21ea4dcb --- /dev/null +++ b/Niconicome/Models/Infrastructure/Network/IPHandler.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Sockets; +using System.Net; +using System.Text; +using System.Threading.Tasks; +using Niconicome.Models.Domain.Local.Server.Core; + +namespace Niconicome.Models.Infrastructure.Network +{ + public class IPHandler : IIPHandler + { + public string GetMyLocalIP() + { + string hostname = Dns.GetHostName(); + IPAddress[] adrList = Dns.GetHostAddresses(hostname); + + foreach (var address in adrList) + { + if (address.AddressFamily == AddressFamily.InterNetwork) + { + return address.ToString(); + } + } + + return "localhost"; + } + + } +} diff --git a/Niconicome/Models/Local/Addon/API/Net/Http/Fetch/Fetch.cs b/Niconicome/Models/Local/Addon/API/Net/Http/Fetch/Fetch.cs index fb98ffe0..94b10428 100644 --- a/Niconicome/Models/Local/Addon/API/Net/Http/Fetch/Fetch.cs +++ b/Niconicome/Models/Local/Addon/API/Net/Http/Fetch/Fetch.cs @@ -76,7 +76,8 @@ public async Task FetchAsync(string url, IFetchOption option) if (option.IncludeCredentioals) { optionResponse = await this.http.NicoHttp!.SendAsync(request); - } else + } + else { optionResponse = await this.http.SendAsync(request); } @@ -91,10 +92,11 @@ public async Task FetchAsync(string url, IFetchOption option) HttpResponseMessage message; if (option.IncludeCredentioals) { + message = option?.Method switch { "GET" => await this.http.NicoHttp!.GetAsync(new Uri(url)), - "POST" => await this.http.NicoHttp!.PostAsync(new Uri(url), content!), + "POST" => await this.http.NicoHttp!.PostAsync(new Uri(url), content!, option.Headers), _ => await this.http.NicoHttp!.GetAsync(new Uri(url)), }; } diff --git a/Niconicome/Models/Local/Application/StartUp.cs b/Niconicome/Models/Local/Application/StartUp.cs index 6f271609..7f21ebf3 100644 --- a/Niconicome/Models/Local/Application/StartUp.cs +++ b/Niconicome/Models/Local/Application/StartUp.cs @@ -9,7 +9,6 @@ using NicoIO = Niconicome.Models.Domain.Local.IO; using Niconicome.Models.Local.Settings; using Niconicome.Models.Local.Addon; -using Niconicome.Models.Utils.InitializeAwaiter; using Niconicome.Models.Local.Addon.V2; using Niconicome.Models.Domain.Local.Settings; using Niconicome.Models.Helper.Result; @@ -21,6 +20,11 @@ using Niconicome.Models.Domain.Niconico.Download.Video.V2.Local.HLS; using System.Collections; using System.Collections.Generic; +using Niconicome.Models.Domain.Utils.BackgroundTask; +using Niconicome.Models.Local.State.MessageV2; +using Niconicome.Models.Domain.Network; +using Niconicome.Models.Domain.Local.Store.V2; +using Niconicome.Models.Domain.Utils.Error; namespace Niconicome.Models.Local.Application { @@ -34,19 +38,21 @@ public interface IStartUp class StartUp : IStartUp { - public StartUp(IBackupManager backuphandler, IAutoLogin autoLogin, IToastHandler snackbarHandler, ILogger logger, Resume::IStreamResumer streamResumer, NicoIO::INicoDirectoryIO nicoDirectoryIO, IAddonManager addonManager, IAddonInstallManager installManager, ISettingsContainer settingsConainer, IServer server,ISegmentDirectoryHandler segmentDirectoryHandler) + public StartUp(IBackupManager backuphandler, IAutoLogin autoLogin, IToastHandler snackbarHandler, ILogger logger, NicoIO::INicoDirectoryIO nicoDirectoryIO, IAddonManager addonManager, IAddonInstallManager installManager, ISettingsContainer settingsConainer, IServer server, ISegmentDirectoryHandler segmentDirectoryHandler, IBackgroundTaskManager backgroundTaskManager, IMessageHandler messageHandler, INetWorkState netWorkState) { this._backuphandler = backuphandler; this._autoLogin = autoLogin; this._snackbarHandler = snackbarHandler; this._logger = logger; - this._streamResumer = streamResumer; this._nicoDirectoryIO = nicoDirectoryIO; this._addonManager = addonManager; this._installManager = installManager; this._settingsConainer = settingsConainer; this._server = server; this._segmentDirectoryHandler = segmentDirectoryHandler; + this._backgroundTaskManager = backgroundTaskManager; + this._messageHandler = messageHandler; + this._netWorkState = netWorkState; this.DeleteInvalidbackup(); } @@ -60,8 +66,6 @@ public StartUp(IBackupManager backuphandler, IAutoLogin autoLogin, IToastHandler private readonly ILogger _logger; - private readonly Resume::IStreamResumer _streamResumer; - private readonly NicoIO::INicoDirectoryIO _nicoDirectoryIO; private readonly IAddonManager _addonManager; @@ -74,6 +78,12 @@ public StartUp(IBackupManager backuphandler, IAutoLogin autoLogin, IToastHandler private readonly ISegmentDirectoryHandler _segmentDirectoryHandler; + private readonly IBackgroundTaskManager _backgroundTaskManager; + + private readonly IMessageHandler _messageHandler; + + private readonly INetWorkState _netWorkState; + #endregion /// @@ -86,12 +96,15 @@ public StartUp(IBackupManager backuphandler, IAutoLogin autoLogin, IToastHandler /// public void RunStartUptasks() { - Task.Run(async () => + this._messageHandler.AppendMessage("スタートアップタスクを開始します。", LocalConstant.SystemMessageDispacher); + + this._backgroundTaskManager.AddTask(this.StartServer); + this._backgroundTaskManager.AddTask(this.RemoveTmpFolder); + this._backgroundTaskManager.AddTask(async () => await this.LoadAddon()); + var task = this._backgroundTaskManager.AddTask(async () => await this.Autologin()); + task.IsDone.Subscribe((x) => { - this.StartServer(); - this.RemoveTmpFolder(); - await this.LoadAddon(); - await this.Autologin(); + this._messageHandler.AppendMessage("スタートアップタスクが完了しました。", LocalConstant.SystemMessageDispacher); }); } @@ -110,7 +123,7 @@ private void RemoveTmpFolder() if (maxTmp < 0) maxTmp = 20; IAttemptResult> result = this._segmentDirectoryHandler.GetAllSegmentDirectoryInfos(); - if (!result.IsSucceeded||result.Data is null) + if (!result.IsSucceeded || result.Data is null) { return; } @@ -150,9 +163,10 @@ private void StartServer() private async Task Autologin() { if (!this._autoLogin.IsAUtologinEnable) return; + if (!await this._netWorkState.IsNetWorkAvailable()) return; if (!this._autoLogin.Canlogin()) { - this._snackbarHandler.Enqueue("自動ログインが出来ません。"); + this._messageHandler.AppendMessage("自動ログインが出来ません。",LocalConstant.SystemMessageDispacher,ErrorLevel.Warning); return; } @@ -165,18 +179,18 @@ private async Task Autologin() catch (Exception e) { this._logger.Error("自動ログインに失敗しました。", e); - this._snackbarHandler.Enqueue("自動ログインに失敗しました。"); + this._messageHandler.AppendMessage("自動ログインに失敗しました。", LocalConstant.SystemMessageDispacher,ErrorLevel.Error); return; } if (!result) { - this._snackbarHandler.Enqueue("自動ログインに失敗しました。"); + this._messageHandler.AppendMessage("自動ログインに失敗しました。", LocalConstant.SystemMessageDispacher,ErrorLevel.Error); return; } else { - this._snackbarHandler.Enqueue("自動ログインに成功しました。"); + this._messageHandler.AppendMessage("自動ログインに成功しました。", LocalConstant.SystemMessageDispacher,ErrorLevel.Log); this.RaiseLoginSucceeded(); } } @@ -196,7 +210,11 @@ private async Task LoadAddon() { this._addonManager.InitializeAddons(); await this._installManager.InstallEssensialAddons(); - await this._addonManager.CheckForUpdates(); + if (await this._netWorkState.IsNetWorkAvailable()) + { + await this._addonManager.CheckForUpdates(); + } + } } } diff --git a/Niconicome/Models/Local/External/ExternalAppUtilsV2.cs b/Niconicome/Models/Local/External/ExternalAppUtilsV2.cs index 13c36cbb..a3574492 100644 --- a/Niconicome/Models/Local/External/ExternalAppUtilsV2.cs +++ b/Niconicome/Models/Local/External/ExternalAppUtilsV2.cs @@ -12,6 +12,7 @@ using Niconicome.Models.Domain.Utils.Error; using Niconicome.Models.Helper.Result; using Niconicome.Models.Local.External.Error; +using Niconicome.Models.Local.State; using Niconicome.Models.Playlist.V2.Manager; namespace Niconicome.Models.Local.External @@ -56,13 +57,14 @@ public interface IExternalAppUtilsV2 public class ExternalAppUtilsV2 : IExternalAppUtilsV2 { - public ExternalAppUtilsV2(IErrorHandler errorHandler, ICommandExecuter commandExecuter, ISettingsContainer settingsContainer, IVideoStore videoStore, IPlaylistManager playlistManager) + public ExternalAppUtilsV2(IErrorHandler errorHandler, ICommandExecuter commandExecuter, ISettingsContainer settingsContainer, IVideoStore videoStore, IPlaylistManager playlistManager,ILocalState localState) { this._errorHandler = errorHandler; this._commandExecuter = commandExecuter; this._settingsContainer = settingsContainer; this._videoStore = videoStore; this._playlistManager = playlistManager; + this._localState = localState; } @@ -78,6 +80,8 @@ public ExternalAppUtilsV2(IErrorHandler errorHandler, ICommandExecuter commandEx private readonly IPlaylistManager _playlistManager; + private readonly ILocalState _localState; + #endregion #region Method @@ -184,7 +188,16 @@ private IAttemptResult OpenPlayer(string appPath, IVideoInfo videoInfo) return AttemptResult.Fail(this._errorHandler.GetMessageForResult(ExternalAppUtilsV2Error.VideoIsNotDownloaded, videoInfo.NiconicoId)); } - var path = videoInfo.FilePath.Replace(@"\\?\", string.Empty); + string path; + + if (videoInfo.IsDMS) + { + path = $"http://localhost:{this._localState.Port}/niconicome/watch/v1/{videoInfo.PlaylistID}/{videoInfo.NiconicoId}/main.m3u8"; + } + else + { + path = videoInfo.FilePath.Replace(@"\\?\", string.Empty); + } this.AddVideoToHistory(videoInfo.NiconicoId, path); @@ -204,6 +217,7 @@ private IAttemptResult SendToAppCommand(string appPath, string argBase, IVideoIn .Replace("", NetConstant.NiconicoWatchUrl + videoInfo.NiconicoId) .Replace("", NetConstant.NiconicoShortUrl + videoInfo.NiconicoId) .Replace("", videoInfo.NiconicoId) + .Replace("", $"http://localhost:{this._localState.Port}/niconicome/watch/v1/{videoInfo.PlaylistID}/{videoInfo.NiconicoId}/main.m3u8") ; return this._commandExecuter.Execute(appPath, constructedArg); diff --git a/Niconicome/Models/Local/State/BlazorPageManager.cs b/Niconicome/Models/Local/State/BlazorPageManager.cs index 6bcf7f6f..294f56e0 100644 --- a/Niconicome/Models/Local/State/BlazorPageManager.cs +++ b/Niconicome/Models/Local/State/BlazorPageManager.cs @@ -14,28 +14,14 @@ public interface IBlazorPageManager /// /// /// - void RequestBlazorToNavigate(string url, BlazorWindows window); + void RequestBlazorToNavigate(string url); /// /// NavigationManagerを登録 /// /// /// - void RegisterNavigationManager(BlazorWindows window, NavigationManager navigationManager); - - /// - /// NavigationManagerを登録解除 - /// - /// - /// - void UnRegisterNavigationManager(BlazorWindows window); - - /// - /// 遷移すべきページを取得 - /// - /// - /// - string GetPageToNavigate(BlazorWindows window); + void RegisterNavigationManager(NavigationManager navigationManager); /// /// リロードが要求された場合 @@ -55,63 +41,22 @@ public class BlazorPageManager : IBlazorPageManager private readonly Dictionary _pages = new(); - private readonly Dictionary _navigations = new(); + private NavigationManager? _navigation; #endregion #region Method - public void RequestBlazorToNavigate(string url, BlazorWindows window) + public void RequestBlazorToNavigate(string url) { - if (this._pages.ContainsKey(window)) - { - this._pages[window] = url; - } - else - { - this._pages.Add(window, url); - } - - if (this._navigations.ContainsKey(window)) - { - this._navigations[window].NavigateTo(url); - } - + this._navigation?.NavigateTo(url); this.CurrentRequestedPage = url; } - public string GetPageToNavigate(BlazorWindows window) - { - if (this._pages.TryGetValue(window, out string? page)) - { - return page; - } - else - { - this._pages.Add(window, "/"); - return "/"; - } - } - - public void RegisterNavigationManager(BlazorWindows window, NavigationManager navigation) - { - if (this._navigations.ContainsKey(window)) - { - this._navigations[window] = navigation; - return; - } - - this._navigations.Add(window, navigation); - } - public void UnRegisterNavigationManager(BlazorWindows window) + public void RegisterNavigationManager(NavigationManager navigation) { - if (!this._navigations.ContainsKey(window)) - { - return; - } - - this._navigations.Remove(window); + this._navigation = navigation; } @@ -125,7 +70,7 @@ public void ReloadRequested(string currentURL) #region Props - public string CurrentRequestedPage { get; private set; } = "/"; + public string CurrentRequestedPage { get; private set; } = "/videos"; #endregion } diff --git a/Niconicome/Models/Local/State/Tab/V1/Tab.cs b/Niconicome/Models/Local/State/Tab/V1/Tab.cs new file mode 100644 index 00000000..d1460274 --- /dev/null +++ b/Niconicome/Models/Local/State/Tab/V1/Tab.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Niconicome.Models.Local.State.Tab.V1 +{ + public interface ITab + { + /// + /// タブの名前 + /// + string Name { get; } + + /// + /// タブのID + /// + string ID { get; } + + /// + /// タブの種類 + /// + TabType Type { get; } + + /// + /// タブを閉じる + /// + void Close(); + } + + public class Tab : ITab + { + private readonly Action _close; + + public Tab(string name, TabType type, Action close, string iD) + { + this.Name = name; + this._close = close; + this.ID = iD; + this.Type = type; + } + + public string Name { get; init; } + + public string ID { get; init; } + + public TabType Type { get; init; } + + public void Close() + { + this._close(); + } + } +} diff --git a/Niconicome/Models/Local/State/Tab/V1/TabControler.cs b/Niconicome/Models/Local/State/Tab/V1/TabControler.cs new file mode 100644 index 00000000..9412c0a6 --- /dev/null +++ b/Niconicome/Models/Local/State/Tab/V1/TabControler.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Niconicome.Extensions.System.List; +using Niconicome.Models.Domain.Utils.StringHandler; + +namespace Niconicome.Models.Local.State.Tab.V1 +{ + public interface ITabControler + { + /// + /// タブを開く + /// + /// + /// + ITab Open(TabType type); + + /// + /// 開いているタブ + /// + ReadOnlyObservableCollection Tabs { get; } + } + + public class TabControler : ITabControler + { + public TabControler(IStringHandler stringHandler) + { + this._stringHandler = stringHandler; + this.Open(TabType.Main); + } + + private readonly ObservableCollection tabs = new(); + + private readonly IStringHandler _stringHandler; + + public ITab Open(TabType type) + { + string id = Guid.NewGuid().ToString(); + ITab tab = new Tab(this._stringHandler.GetContent(type), type, () => this.tabs.RemoveAll(t => t.Type != TabType.Main && t.ID == id), id); + this.tabs.Add(tab); + return tab; + } + + public ReadOnlyObservableCollection Tabs => new(this.tabs); + } + + public enum TabType + { + [StringEnum("動画リスト")] + Main, + [StringEnum("設定")] + Settings, + [StringEnum("ダウンロード")] + Download, + [StringEnum("アドオン")] + Addon + } +} diff --git a/Niconicome/Models/Local/Timer/DlTimer.cs b/Niconicome/Models/Local/Timer/DlTimer.cs index 3f1a1b86..d00b80a7 100644 --- a/Niconicome/Models/Local/Timer/DlTimer.cs +++ b/Niconicome/Models/Local/Timer/DlTimer.cs @@ -1,8 +1,11 @@ using System; +using Niconicome.Models.Domain.Local.Settings; using Niconicome.Models.Domain.Utils; +using Niconicome.Models.Domain.Utils.BackgroundTask; using Niconicome.Models.Domain.Utils.Event; using Niconicome.Models.Local.Settings; using Niconicome.Models.Network.Download; +using Niconicome.Models.Utils.Reactive; using Niconicome.ViewModels; using Reactive.Bindings; @@ -13,7 +16,12 @@ interface IDlTimer /// /// 有効フラグ /// - ReactiveProperty IsEnabled { get; } + IBindableProperty IsEnabled { get; } + + /// + /// 24時間ごとに繰り返すかどうか + /// + IBindableProperty IsRepeatByDayEnable { get; } /// /// イベント発火時刻 @@ -29,37 +37,54 @@ interface IDlTimer class DlTimer : BindableBase, IDlTimer { - public DlTimer(IEventManager manager, ILogger logger, ILocalSettingsContainer settingsContainer) + public DlTimer(ISettingsContainer settingsContainer, IBackgroundTaskManager backgroundTaskManager) { - this.manager = manager; - this.logger = logger; + this._backgroundTaskManager = backgroundTaskManager; this.settingsContainer = settingsContainer; + this.IsEnabled.Subscribe(value => this.ChangeState(value)); + this.IsRepeatByDayEnable = new BindableProperty(this.settingsContainer.GetOnlyValue(SettingNames.IsDlTImerEveryDayEnable, false).Data) + .Subscribe(value => + { + if (this._isRepeatByDayEnable is null) + { + var result = this.settingsContainer.GetSetting(SettingNames.IsDlTImerEveryDayEnable, false); + if (!result.IsSucceeded || result.Data is null) return; + this._isRepeatByDayEnable = result.Data; + } + + if (this._isRepeatByDayEnable.Value == value) return; + + this._isRepeatByDayEnable.Value = value; + this.IsRepeatByDayEnable!.Value = value; + }); } #region field - private readonly IEventManager manager; + private readonly IBackgroundTaskManager _backgroundTaskManager; - private readonly ILogger logger; + private readonly ISettingsContainer settingsContainer; - private readonly ILocalSettingsContainer settingsContainer; + private ISettingInfo? _isRepeatByDayEnable; - private string? eventID; + private BackgroundTask? _task; private DateTime dt = DateTime.Now; - private Action? dlAction; + private Action? _dlAction; #endregion #region Props - public ReactiveProperty IsEnabled { get; init; } = new(); + public IBindableProperty IsEnabled { get; init; } = new BindableProperty(false); public DateTime TrigggeredDT => this.dt; + public IBindableProperty IsRepeatByDayEnable { get; init; } + #endregion #region Methods @@ -67,7 +92,7 @@ public DlTimer(IEventManager manager, ILogger logger, ILocalSettingsContainer se public void Set(DateTime dt, Action dlAction) { this.dt = dt; - this.dlAction = dlAction; + this._dlAction = dlAction; this.Reset(); this.ChangeState(this.IsEnabled.Value); } @@ -82,7 +107,7 @@ public void Set(DateTime dt, Action dlAction) /// private void ChangeState(bool isEnabled) { - if (this.dlAction is null) + if (this._dlAction is null) { return; } @@ -94,25 +119,24 @@ private void ChangeState(bool isEnabled) this.dt = DateTime.Now + TimeSpan.FromDays(1); } - this.eventID = this.manager.Regster(() => + this._task = this._backgroundTaskManager.AddTimerTask(() => { - this.dlAction(); + this._dlAction(); - if (this.settingsContainer.GetReactiveBoolSetting(SettingsEnum.DlTimerEveryDay).Value) + if (this.IsRepeatByDayEnable.Value) { - this.Set(this.dt + TimeSpan.FromDays(1), this.dlAction); - } else + this.Set(this.dt + TimeSpan.FromDays(1), this._dlAction); + } + else { this.IsEnabled.Value = false; } - }, this.dt, ex => - { - this.logger.Error("タイマーDLに失敗しました。", ex); - }); + }, this.dt); } - else if (this.eventID is not null) + else if (this._task is not null) { - this.manager.Cancel(this.eventID); + this._backgroundTaskManager.CancelTask(this._task.TaskID); + this._task = null; } } @@ -121,10 +145,10 @@ private void ChangeState(bool isEnabled) /// private void Reset() { - if (this.eventID is not null) - { - this.manager.Cancel(this.eventID); - } + if (this._task is null) return; + + this._backgroundTaskManager.CancelTask(this._task.TaskID); + this._task = null; } #endregion diff --git a/Niconicome/Models/Network/Download/Actions/PostDownloadActions.cs b/Niconicome/Models/Network/Download/Actions/PostDownloadActions.cs index b2aae768..2971c930 100644 --- a/Niconicome/Models/Network/Download/Actions/PostDownloadActions.cs +++ b/Niconicome/Models/Network/Download/Actions/PostDownloadActions.cs @@ -6,7 +6,7 @@ namespace Niconicome.Models.Network.Download.Actions { - enum PostDownloadActions + public enum PostDownloadActions { None, Shutdown, diff --git a/Niconicome/Models/Network/Download/Actions/V2/PostDownloadActionsManager.cs b/Niconicome/Models/Network/Download/Actions/V2/PostDownloadActionsManager.cs new file mode 100644 index 00000000..d56953f8 --- /dev/null +++ b/Niconicome/Models/Network/Download/Actions/V2/PostDownloadActionsManager.cs @@ -0,0 +1,92 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Niconicome.Models.Domain.Local.Machine; +using Niconicome.Models.Domain.Local.Settings; +using Niconicome.Models.Domain.Utils.Error; +using Niconicome.Models.Domain.Utils.NicoLogger; +using Niconicome.Models.Utils.Reactive; +using Niconicome.ViewModels.Setting.Utils; +using Err = Niconicome.Models.Network.Download.Actions.V2.PostDownloadActionsManagerError; + +namespace Niconicome.Models.Network.Download.Actions.V2 +{ + interface IPostDownloadActionssManager + { + /// + /// DL完了後のアクション + /// + IBindableProperty PostDownloadAction { get; } + + /// + /// 設定されたアクションを実行する + /// + void HandleAction(); + } + + public class PostDownloadActionsManager : IPostDownloadActionssManager + { + public PostDownloadActionsManager(IComPowerManager comPowerManager, ISettingsContainer conainer, IErrorHandler errorHandler) + { + this._comPower = comPowerManager; + this._settingsContainer = conainer; + this._errorHandler = errorHandler; + this.PostDownloadAction = new BindableProperty(this._settingsContainer.GetOnlyValue(SettingNames.PostDownloadAction, PostDownloadActions.None).Data) + .Subscribe(value => + { + if (this._postDownloadAction is null) + { + var result = this._settingsContainer.GetSetting(SettingNames.PostDownloadAction, PostDownloadActions.None); + if (!result.IsSucceeded || result.Data is null) return; + this._postDownloadAction = result.Data; + } + + if (this._postDownloadAction.Value == value) return; + this._postDownloadAction.Value = value; + this.PostDownloadAction!.Value = value; + + }); + } + + private readonly IComPowerManager _comPower; + + private readonly ISettingsContainer _settingsContainer; + + private readonly IErrorHandler _errorHandler; + + private ISettingInfo? _postDownloadAction; + + public IBindableProperty PostDownloadAction { get; init; } + + public void HandleAction() + { + if (this.PostDownloadAction is null) return; + PostDownloadActions action = this.PostDownloadAction.Value; + + if (action == PostDownloadActions.Shutdown) + { + this._errorHandler.HandleError(Err.ShutDown); + this._comPower.Shutdown(); + } + else if (action == PostDownloadActions.Restart) + { + this._errorHandler.HandleError(Err.Restart); + this._comPower.Restart(); + } + else if (action == PostDownloadActions.LogOff) + { + this._errorHandler.HandleError(Err.LogOff); + this._comPower.LogOff(); + } + else if (action == PostDownloadActions.Sleep) + { + this._errorHandler.HandleError(Err.Sleep); + this._comPower.Sleep(); + } + } + + + } +} diff --git a/Niconicome/Models/Network/Download/Actions/V2/PostDownloadActionsManagerError.cs b/Niconicome/Models/Network/Download/Actions/V2/PostDownloadActionsManagerError.cs new file mode 100644 index 00000000..a49f6353 --- /dev/null +++ b/Niconicome/Models/Network/Download/Actions/V2/PostDownloadActionsManagerError.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Niconicome.Models.Domain.Utils.Error; + +namespace Niconicome.Models.Network.Download.Actions.V2 +{ + public enum PostDownloadActionsManagerError + { + [ErrorEnum(ErrorLevel.Log, "コンピューターをシャットダウンします。")] + ShutDown, + [ErrorEnum(ErrorLevel.Log, "コンピューターを再起動します。")] + Restart, + [ErrorEnum(ErrorLevel.Log, "Windowsからログオフします。")] + LogOff, + [ErrorEnum(ErrorLevel.Log, "コンピューターを休止状態に移行します。")] + Sleep, + } +} diff --git a/Niconicome/Models/Network/Download/ContentDownloadHelper.cs b/Niconicome/Models/Network/Download/ContentDownloadHelper.cs index 6fe9f56a..2965f437 100644 --- a/Niconicome/Models/Network/Download/ContentDownloadHelper.cs +++ b/Niconicome/Models/Network/Download/ContentDownloadHelper.cs @@ -13,11 +13,13 @@ using Niconicome.Models.Domain.Playlist; using Niconicome.Models.Domain.Utils; using Niconicome.Models.Helper.Result; +using Niconicome.Models.Network.Download.Modification.Video; using Niconicome.Models.Network.Watch; using DDL = Niconicome.Models.Domain.Niconico.Download.Description; using Tdl = Niconicome.Models.Domain.Niconico.Download.Thumbnail; using V2Comment = Niconicome.Models.Domain.Niconico.Download.Comment.V2.Integrate; -using Vdl = Niconicome.Models.Domain.Niconico.Download.Video.V2; +using Vdl = Niconicome.Models.Domain.Niconico.Download.Video.V3; +using VdlLegacy = Niconicome.Models.Domain.Niconico.Download.Video.V2; namespace Niconicome.Models.Network.Download { @@ -36,7 +38,7 @@ public interface IContentDownloadHelper public class ContentDownloadHelper : IContentDownloadHelper { - public ContentDownloadHelper(ILogger logger, IDomainModelConverter converter, IWatchPageInfomationHandler watchPageInfomation, ILocalFileHandler localFileHandler, IPathOrganizer pathOrganizer,INiconicomeDirectoryIO directoryIO) + public ContentDownloadHelper(ILogger logger, IDomainModelConverter converter, IWatchPageInfomationHandler watchPageInfomation, ILocalFileHandler localFileHandler, IPathOrganizer pathOrganizer, INiconicomeDirectoryIO directoryIO, IVideoModificationManager videoModificationManager) { this.converter = converter; this.logger = logger; @@ -44,6 +46,7 @@ public ContentDownloadHelper(ILogger logger, IDomainModelConverter converter, IW this._localFileHandler = localFileHandler; this._pathOrganizer = pathOrganizer; this._directoryIO = directoryIO; + this._videoModificationManager = videoModificationManager; } #region DI @@ -59,12 +62,15 @@ public ContentDownloadHelper(ILogger logger, IDomainModelConverter converter, IW private readonly INiconicomeDirectoryIO _directoryIO; + private readonly IVideoModificationManager _videoModificationManager; + #endregion #region Methods public async Task> TryDownloadContentAsync(IVideoInfo videoInfo, IDownloadSettings setting, Action OnMessage, CancellationToken token) - { + {; + var context = new DownloadContext(setting.NiconicoId); context.RegisterMessageHandler(OnMessage); @@ -244,11 +250,18 @@ public async Task> TryDownloadContentAsync(IVid /// private async Task TryDownloadVideoAsync(IDownloadSettings settings, Action onMessage, IDomainVideoInfo videoInfo, IDownloadContext context, CancellationToken token) { - var videoDownloader = DIFactory.Provider.GetRequiredService(); IAttemptResult result; try { - result = await videoDownloader.DownloadVideoAsync(settings, onMessage, videoInfo, token); + if (videoInfo.DmcInfo.IsDMS) + { + IAttemptResult resultNew = await DIFactory.Provider.GetRequiredService().DownloadVideoAsync(settings, onMessage, videoInfo, token); + result = resultNew.IsSucceeded ? AttemptResult.Succeeded((uint)resultNew.Data) : AttemptResult.Fail(resultNew.Message); + } + else + { + result = await DIFactory.Provider.GetRequiredService().DownloadVideoAsync(settings, onMessage, videoInfo, token); + } } catch (Exception e) { @@ -259,6 +272,7 @@ private async Task TryDownloadVideoAsync(IDownloadSettings setti context.ActualVerticalResolution = result.Data; if (result.IsSucceeded) { + await this.ModifyVideo(settings, videoInfo, onMessage, token); return AttemptResult.Succeeded(); } else @@ -267,6 +281,20 @@ private async Task TryDownloadVideoAsync(IDownloadSettings setti } } + /// + /// DL後処理を行う + /// + /// + /// + /// + /// + private async Task ModifyVideo(IDownloadSettings settings, IDomainVideoInfo videoInfo, Action onMessage, CancellationToken ct) + { + if (!settings.IsModifyVideoEnable) return; + string filePath = this.GetVideoFilePath(settings, videoInfo); + await this._videoModificationManager.ModifyVideo(videoInfo.Id, settings.PlaylistID.ToString(), filePath, onMessage, ct); + } + /// /// 動画情報をダウンロードする /// diff --git a/Niconicome/Models/Network/Download/DownloadSettings.cs b/Niconicome/Models/Network/Download/DownloadSettings.cs index bab39d72..1ffeaeb7 100644 --- a/Niconicome/Models/Network/Download/DownloadSettings.cs +++ b/Niconicome/Models/Network/Download/DownloadSettings.cs @@ -138,6 +138,11 @@ public interface IDownloadSettings /// bool AlwaysSkipEconomyDownload { get; } + /// + /// DLの後処理を行うかどうか + /// + bool IsModifyVideoEnable { get; } + /// /// 動画ID /// @@ -320,6 +325,8 @@ public record DownloadSettings : IDownloadSettings public bool AlwaysSkipEconomyDownload { get; set; } + public bool IsModifyVideoEnable { get; set; } + public uint VerticalResolution { get; set; } public int PlaylistID { get; set; } diff --git a/Niconicome/Models/Network/Download/DownloadSettingsHandler.cs b/Niconicome/Models/Network/Download/DownloadSettingsHandler.cs index 23cf8ee2..da9df34a 100644 --- a/Niconicome/Models/Network/Download/DownloadSettingsHandler.cs +++ b/Niconicome/Models/Network/Download/DownloadSettingsHandler.cs @@ -166,6 +166,7 @@ public DownloadSettings CreateDownloadSettings() bool omitXmlDec = this._container.GetSetting(SettingNames.IsOmittingXmlDeclarationIsEnable, false).Data!.Value; bool skipEco = this._container.GetOnlyValue(SettingNames.AlwaysSkipEconomyDownload, false).Data!; bool skipEcoWhenPremium = this._container.GetOnlyValue(SettingNames.SkipEconomyDownloadIfPremiumExists, false).Data!; + bool modifyVideo = this._container.GetOnlyValue(SettingNames.IsVideoModificationEnable, false).Data!; //ファイル系 @@ -273,6 +274,7 @@ public DownloadSettings CreateDownloadSettings() ExternalDownloaderConditionSetting = externalDownloader, SkipEconomyDownloadIfPremiumExists = skipEcoWhenPremium, AlwaysSkipEconomyDownload = skipEco, + IsModifyVideoEnable = modifyVideo, }; } diff --git a/Niconicome/Models/Network/Download/Modification/Video/VideoModificationManager.cs b/Niconicome/Models/Network/Download/Modification/Video/VideoModificationManager.cs new file mode 100644 index 00000000..f12ed5af --- /dev/null +++ b/Niconicome/Models/Network/Download/Modification/Video/VideoModificationManager.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Niconicome.Extensions.System; +using Niconicome.Models.Const; +using Niconicome.Models.Domain.Local.External.Software.NiconicomeProcess; +using Niconicome.Models.Domain.Local.Settings; +using Niconicome.Models.Domain.Niconico.Video.Infomations; +using Niconicome.Models.Domain.Utils; +using Niconicome.Models.Helper.Result; +using Niconicome.Models.Local.State; + +namespace Niconicome.Models.Network.Download.Modification.Video +{ + public interface IVideoModificationManager + { + /// + /// ダウンロードの後処理を行う + /// + /// + /// + /// + /// + /// + Task ModifyVideo(string niconicoID, string playlistID, string videoFilePath, Action onMessage, CancellationToken ct); + } + + public class VideoModificationManager : IVideoModificationManager + { + public VideoModificationManager(ISettingsContainer settingsContainer, ILocalState localState, IProcessManager processManager) + { + this._settingsContainer = settingsContainer; + this._localState = localState; + this._processManager = processManager; + } + + private readonly ISettingsContainer _settingsContainer; + + private readonly ILocalState _localState; + + private readonly IProcessManager _processManager; + + public async Task ModifyVideo(string niconicoID, string playlistID, string videoFilePath, Action onMessage, CancellationToken ct) + { + IAttemptResult softwarePathResult = this._settingsContainer.GetOnlyValue(SettingNames.VideoModificationSoftwarePath, string.Empty); + if (!softwarePathResult.IsSucceeded || string.IsNullOrEmpty(softwarePathResult.Data)) + { + return; + } + + IAttemptResult argsResult = this._settingsContainer.GetOnlyValue(SettingNames.VideoModificationSoftwareParam, string.Empty); + if (!argsResult.IsSucceeded || string.IsNullOrEmpty(argsResult.Data)) + { + return; + } + + string url = string.Format(NetConstant.WatchAddressV1, this._localState.Port, playlistID, niconicoID); + + string arg = argsResult.Data + .Replace("", url) + .Replace("", videoFilePath); + + await this._processManager.StartProcessAsync(softwarePathResult.Data, arg, false, onMessage, ct); + } + } +} diff --git a/Niconicome/Models/Network/Video/ThumbnailUtility.cs b/Niconicome/Models/Network/Video/ThumbnailUtility.cs index 1a5d6fa3..5729c679 100644 --- a/Niconicome/Models/Network/Video/ThumbnailUtility.cs +++ b/Niconicome/Models/Network/Video/ThumbnailUtility.cs @@ -12,7 +12,6 @@ using Niconicome.Models.Domain.Utils.Error; using Niconicome.Models.Helper.Result; using Niconicome.Models.Network.Video.Error; -using Niconicome.Models.Utils.InitializeAwaiter; using Windows.Networking.Proximity; namespace Niconicome.Models.Network.Video diff --git a/Niconicome/Models/Playlist/V2/Manager/Helper/LocalVideoLoader.cs b/Niconicome/Models/Playlist/V2/Manager/Helper/LocalVideoLoader.cs index fc35b6e4..2d234a1a 100644 --- a/Niconicome/Models/Playlist/V2/Manager/Helper/LocalVideoLoader.cs +++ b/Niconicome/Models/Playlist/V2/Manager/Helper/LocalVideoLoader.cs @@ -81,8 +81,8 @@ public async Task SetPathAsync(IEnumerable videos, b if (string.IsNullOrEmpty(folderPath)) { folderPath = this.GetDownlaodDirectory(this._playlistVideoContainer.CurrentSelectedPlaylist); - this._playlistVideoContainer.CurrentSelectedPlaylist.TemporaryFolderPath = folderPath; } + this._playlistVideoContainer.CurrentSelectedPlaylist.TemporaryFolderPath = folderPath; ///削除動画のサムネを保存 await this._thumbnailUtility.DownloadDeletedVideoThumbAsync(); @@ -192,22 +192,33 @@ private IAttemptResult GetFilePath(string niconicoID, string folderPath) { this._cachedFiles.AddRange(this._directoryIO.GetFiles(folderPath, $"*{FileFolder.Mp4FileExt}", true).Select(p => Path.Combine(folderPath, p)).ToList()); this._cachedFiles.AddRange(this._directoryIO.GetFiles(folderPath, $"*{FileFolder.TsFileExt}", true).Select(p => Path.Combine(folderPath, p)).ToList()); + this._cachedFolders.AddRange(this._directoryIO.GetDirectorys(folderPath).Select(p => Path.Combine(folderPath, p)).ToList()); } } + //stream.jsonを確認 + string? firstFolder = this._cachedFolders.FirstOrDefault(p => p.Contains(niconicoID)); + if (firstFolder is not null) + { + return AttemptResult.Succeeded(Path.Combine(firstFolder, "stream.json")); + } + string? firstMp4 = this._cachedFiles.FirstOrDefault(p => p.Contains(niconicoID)); + //.mp4ファイルを確認 if (firstMp4 is not null) { return AttemptResult.Succeeded(firstMp4); } - else + + string? firstTS = this._cachedFiles.FirstOrDefault(p => p.Contains(niconicoID)); + if (firstTS is not null) //.tsファイルを確認 { - string? firstTS = this._cachedFiles.FirstOrDefault(p => p.Contains(niconicoID)); - if (firstTS is not null) return AttemptResult.Succeeded(firstTS); + return AttemptResult.Succeeded(firstTS); } + return AttemptResult.Fail(); } diff --git a/Niconicome/Models/Utils/InitializeAwaiter/AwaiterNames.cs b/Niconicome/Models/Utils/InitializeAwaiter/AwaiterNames.cs deleted file mode 100644 index 75c537e8..00000000 --- a/Niconicome/Models/Utils/InitializeAwaiter/AwaiterNames.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Niconicome.Models.Utils.InitializeAwaiter -{ - public static class AwaiterNames - { - public static string Addon { get; private set; } = "AddonHandler"; - } -} diff --git a/Niconicome/Models/Utils/InitializeAwaiter/InitializeAwaiter.cs b/Niconicome/Models/Utils/InitializeAwaiter/InitializeAwaiter.cs deleted file mode 100644 index fb44218a..00000000 --- a/Niconicome/Models/Utils/InitializeAwaiter/InitializeAwaiter.cs +++ /dev/null @@ -1,121 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Niconicome.Models.Helper.Result; - -namespace Niconicome.Models.Utils.InitializeAwaiter -{ - public interface IInitializeAwaiter - { - - /// - /// 指定した名前の待機タスクを取得 - /// - /// - /// - Task Awaiter { get; } - - /// - /// 完了フラグ - /// - bool IsCompleted { get; } - - /// - /// 待機するクラスを追加 - /// - /// - - IAttemptResult RegisterStep(Type stepsType); - - /// - /// 処理の完了を通知 - /// - /// - /// - - void NotifyCompletedStep(Type stepsType); - } - - public class InitializeAwaiter : IInitializeAwaiter - { - #region field - - private readonly List _steps = new(); - - private TaskCompletionSource _tcs = new(); - - #endregion - - #region Props - - public Task Awaiter - { - get - { - this.CheckIfCompleted(); - return this._tcs.Task; - } - } - - public bool IsCompleted { get; private set; } = false; - - #endregion - - #region Method - - public IAttemptResult RegisterStep(Type stepsType) - { - if (this.Contains(stepsType)) - { - return AttemptResult.Fail("すでにステップが登録されています。"); - } - - this._steps.Add(stepsType); - - return AttemptResult.Succeeded(); - } - - public void NotifyCompletedStep(Type stepsType) - { - if (!this.Contains(stepsType)) - { - return; - } - - this._steps.RemoveAll(s => s == stepsType); - - this.CheckIfCompleted(); - - } - - #endregion - - #region private - - /// - /// タスクが完了していた場合、プロパティーを書き換える - /// - private void CheckIfCompleted() - { - if (this._steps.Count == 0) - { - this.IsCompleted = true; - this._tcs.TrySetResult(); - } - } - - /// - /// ステップを検索 - /// - /// - /// - private bool Contains(Type stepsType) - { - return this._steps.Contains(stepsType); - } - - #endregion - } -} diff --git a/Niconicome/Models/Utils/InitializeAwaiter/InitializeAwaiterHandler.cs b/Niconicome/Models/Utils/InitializeAwaiter/InitializeAwaiterHandler.cs deleted file mode 100644 index 5ee43c6f..00000000 --- a/Niconicome/Models/Utils/InitializeAwaiter/InitializeAwaiterHandler.cs +++ /dev/null @@ -1,89 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Niconicome.Models.Helper.Result; - -namespace Niconicome.Models.Utils.InitializeAwaiter -{ - - public interface IInitializeAwaiterHandler - { - /// - /// ステップを登録 - /// - /// - /// - /// - IAttemptResult RegisterStep(string awaiterName, Type stepsType); - - /// - /// ステップの完了を通知 - /// - /// - /// - - void NotifyCompletedStep(string awaiterName, Type stepstype); - - /// - /// 待機オブジェクトを取得 - /// - /// - /// - Task GetAwaiter(string awaiterName); - } - - public class InitializeAwaiterHandler : IInitializeAwaiterHandler - { - #region field - - private Dictionary _awaiters = new(); - - #endregion - - public IAttemptResult RegisterStep(string awaiterName, Type stepsType) - { - if (!this._awaiters.ContainsKey(awaiterName)) - { - this._awaiters.Add(awaiterName, new InitializeAwaiter()); - } - - IInitializeAwaiter awaiter = this._awaiters[awaiterName]; - return awaiter.RegisterStep(stepsType); - } - - public void NotifyCompletedStep(string awaiterName, Type stepsType) - { - if (!this._awaiters.ContainsKey(awaiterName)) - { - return; - } - - - IInitializeAwaiter awaiter = this._awaiters[awaiterName]; - awaiter.NotifyCompletedStep(stepsType); - - if (awaiter.IsCompleted) - { - this._awaiters.Remove(awaiterName); - } - } - - public Task GetAwaiter(string awaiterName) - { - if (this._awaiters.ContainsKey(awaiterName)) - { - return this._awaiters[awaiterName].Awaiter; - }else - { - var tcs = new TaskCompletionSource(); - tcs.TrySetResult(); - return tcs.Task; - } - } - - - - } -} diff --git a/Niconicome/Models/Utils/Reactive/Command/BindableCommand.cs b/Niconicome/Models/Utils/Reactive/Command/BindableCommand.cs index 26369342..c462b4e7 100644 --- a/Niconicome/Models/Utils/Reactive/Command/BindableCommand.cs +++ b/Niconicome/Models/Utils/Reactive/Command/BindableCommand.cs @@ -66,11 +66,14 @@ public BindableCommand(Action command, params IReadonlyBindablePperty[] so public void Execute(object? parameter) { - try + this._ctx?.Post(_ => { - this._command(); - } - catch { } + try + { + this._command?.Invoke(); + } + catch { } + }, null); } #endregion diff --git a/Niconicome/Models/Utils/WindowTabHelper.cs b/Niconicome/Models/Utils/WindowTabHelper.cs deleted file mode 100644 index bb002b3d..00000000 --- a/Niconicome/Models/Utils/WindowTabHelper.cs +++ /dev/null @@ -1,154 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using System.Windows; -using System.Windows.Controls; -using Niconicome.Models.Const; -using Niconicome.Models.Local.Settings; -using Niconicome.Models.Local.State; -using Niconicome.ViewModels.Mainpage.Tabs; -using Niconicome.Views; -using Niconicome.Views.AddonPage.V2; -using Niconicome.Views.DownloadTask; -using Niconicome.Views.Setting.V2; -using Prism.Ioc; -using Prism.Regions; -using Prism.Services.Dialogs; -using Prism.Unity; - -namespace Niconicome.Models.Utils -{ - public interface IWindowTabHelper - { - /// - /// ダウンロードタスク一覧を開く - /// - /// - void OpenDownloadTaskWindow(IRegionManager regionManager); - - /// - /// アドオンマネージャーを新しいタブで開く - /// - /// - void OpenAddonManager(IRegionManager regionManager); - - /// - /// 設定タブを開く - /// - void OpenSettingsTab(IRegionManager regionManager); - } - - public class WindowTabHelper : IWindowTabHelper - { - - public WindowTabHelper(ILocalState localState, ILocalSettingHandler settingHandler) - { - this._localState = localState; - this._settingHandler = settingHandler; - } - - #region field - - private readonly ILocalState _localState; - - private readonly ILocalSettingHandler _settingHandler; - - #endregion - - #region Methods - public void OpenDownloadTaskWindow(IRegionManager regionManager) - { - - if (Application.Current is not PrismApplication app) return; - - IContainerProvider container = app.Container; - - IRegion region = regionManager.Regions[LocalConstant.TopTabRegionName]; - - - if (this._localState.IsTaskWindowOpen && this._settingHandler.GetBoolSetting(SettingsEnum.SingletonWindows)) - { - foreach (var view in region.Views) - { - if (view is not UserControl c) continue; - if (c.DataContext is not TabViewModelBase vm) continue; - if (vm.ID != LocalConstant.TaskTabID) continue; - - region.Activate(view); - } - } - else - { - var view = container.Resolve(); - region.Add(view); - region.Activate(view); - this._localState.IsTaskWindowOpen = true; - } - - } - - public void OpenAddonManager(IRegionManager regionManager) - { - - if (Application.Current is not PrismApplication app) return; - - IContainerProvider container = app.Container; - - IRegion region = regionManager.Regions[LocalConstant.TopTabRegionName]; - - if (this._localState.IsAddonManagerOpen && this._settingHandler.GetBoolSetting(SettingsEnum.SingletonWindows)) - { - foreach (var view in region.Views) - { - if (view is not UserControl c) continue; - if (c.DataContext is not TabViewModelBase vm) continue; - if (vm.ID != LocalConstant.AddonManagerTabID) continue; - - region.Activate(view); - } - } - else - { - var view = container.Resolve(); - region.Add(view); - region.Activate(view); - this._localState.IsAddonManagerOpen = true; - } - } - - public void OpenSettingsTab(IRegionManager regionManager) - { - - if (Application.Current is not PrismApplication app) return; - - IContainerProvider container = app.Container; - - IRegion region = regionManager.Regions[LocalConstant.TopTabRegionName]; - - - if (this._localState.IsSettingTabOpen && this._settingHandler.GetBoolSetting(SettingsEnum.SingletonWindows)) - { - foreach (var view in region.Views) - { - if (view is not UserControl c) continue; - if (c.DataContext is not TabViewModelBase vm) continue; - if (vm.ID != LocalConstant.SettingTabID) continue; - - region.Activate(view); - } - } - else - { - var view = container.Resolve(); - region.Add(view); - region.Activate(view); - this._localState.IsSettingTabOpen = true; - } - } - - - #endregion - } -} diff --git a/Niconicome/Niconicome.csproj b/Niconicome/Niconicome.csproj index c6c23a23..46829feb 100644 --- a/Niconicome/Niconicome.csproj +++ b/Niconicome/Niconicome.csproj @@ -2,7 +2,7 @@ WinExe - net6.0-windows10.0.22000.0 + net8.0-windows10.0.22000.0 true enable 0.13.3.* @@ -22,16 +22,17 @@ + - + - - - + + + @@ -61,4 +62,9 @@ Resources.Designer.cs + + + + + diff --git a/Niconicome/Properties/Resources.Designer.cs b/Niconicome/Properties/Resources.Designer.cs index 86a21ebf..f3fd3b14 100644 --- a/Niconicome/Properties/Resources.Designer.cs +++ b/Niconicome/Properties/Resources.Designer.cs @@ -60,6 +60,16 @@ internal Resources() { } } + /// + /// (アイコン) に類似した型 System.Drawing.Icon のローカライズされたリソースを検索します。 + /// + internal static System.Drawing.Icon favicon { + get { + object obj = ResourceManager.GetObject("favicon", resourceCulture); + return ((System.Drawing.Icon)(obj)); + } + } + /// /// /*全体のスタイル*/ ///* { diff --git a/Niconicome/Properties/Resources.resx b/Niconicome/Properties/Resources.resx index 76d5a309..cc533277 100644 --- a/Niconicome/Properties/Resources.resx +++ b/Niconicome/Properties/Resources.resx @@ -118,6 +118,9 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + ..\src\app.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + ..\Resources\UserChrome.txt;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;shift_jis diff --git a/Niconicome/ViewModels/Login/LoginBrowserViewModel.cs b/Niconicome/ViewModels/Login/LoginBrowserViewModel.cs index fb4adda9..5894eb71 100644 --- a/Niconicome/ViewModels/Login/LoginBrowserViewModel.cs +++ b/Niconicome/ViewModels/Login/LoginBrowserViewModel.cs @@ -12,23 +12,13 @@ using Utils = Niconicome.Models.Domain.Utils; using System.Threading.Tasks; using Niconicome.ViewModels.Controls; +using WS = Niconicome.Workspaces.Mainpage; namespace Niconicome.ViewModels.Login { class LoginBrowserViewModel { - /// - /// ログイン成功イベント - /// - public event EventHandler? LoginSucceeded; - - /// - /// ログイン成功イベントを発行 - /// - public void RaiseLoginSucceeded() - { - this.LoginSucceeded?.Invoke(this, EventArgs.Empty); - } + } class WebViewBehavior : Behavior @@ -73,12 +63,9 @@ private async void OnNavigate(object? sender, EventArgs e) return; } - var cookies = await this.handler.GetCookiesAsync(@"https://nicovideo.jp"); - - - if (cookies.Any(cookie => cookie.Name == "user_session")) + if (await WS.NiconicoCookieManager.HandleNavigate()) { - await this.SetCookiesAndExitAsync(cookies); + this.Exit(); } } @@ -86,33 +73,27 @@ private async void OnInitialized(object? sender, EventArgs e) { this.handler.Initialize(this.AssociatedObject.CoreWebView2); - var cookies = await this.handler.GetCookiesAsync(@"https://nicovideo.jp"); + WS.NiconicoCookieManager.Wire(this.handler); - if (cookies.Any(cookie => cookie.Name == "user_session" && cookie.Expires > DateTime.Now)) + if (await WS.NiconicoCookieManager.IsLoggedIn()) { var result = await MaterialMessageBox.Show("有効なセッションが存在します。ログインをスキップしますか?", MessageBoxButtons.Yes | MessageBoxButtons.No, MessageBoxIcons.Question); if (result == MaterialMessageBoxResult.Yes) { - await this.SetCookiesAndExitAsync(cookies); + this.Exit(); return; } } - await this.handler.DeleteBrowserCookiesAsync( @"https://nicovideo.jp"); + await this.handler.DeleteBrowserCookiesAsync(@"https://nicovideo.jp"); this.isInitializeCompleted = true; } - private async Task SetCookiesAndExitAsync(List cookies) + private void Exit() { - var cookieManager = DIFactory.Provider.GetRequiredService(); - foreach (var cookie in cookies) - { - cookieManager.AddCookie(cookie.Name, cookie.Value); - } - await NiconicoContext.Context.RefreshUser(); - (this.AssociatedObject.DataContext as LoginBrowserViewModel)?.RaiseLoginSucceeded(); + WS.NiconicoCookieManager.UnWire(); this.Window?.Close(); } } diff --git a/Niconicome/ViewModels/Login/LoginWindowViewModel.cs b/Niconicome/ViewModels/Login/LoginWindowViewModel.cs deleted file mode 100644 index 10da96a5..00000000 --- a/Niconicome/ViewModels/Login/LoginWindowViewModel.cs +++ /dev/null @@ -1,249 +0,0 @@ -using System; -using System.Linq; -using System.Text; -using System.Windows; -using System.Windows.Controls; -using Microsoft.Xaml.Behaviors; -using Niconicome.Workspaces; -using Niconicome.Models.Auth; -using System.Diagnostics; -using Niconicome.Views; -using Utils = Niconicome.Models.Domain.Utils; -using Niconicome.Models.Domain.Niconico; -using Niconicome.ViewModels.Mainpage.Subwindows; -using WS = Niconicome.Workspaces; - -namespace Niconicome.ViewModels.Login -{ - class LoginWindowViewModel : BindableBase - { - public LoginWindowViewModel() - { - this.LoginCommand = new CommandBase( - (object? arg) => !this.isLoginAttempting && !this.IsLogin&&this.LoginAttemptCount<=this.MaxLoginAttempts, - async (Window? window) => - { - - //ログイン回数確認 - if (this.LoginAttemptCount > this.MaxLoginAttempts) - { - this.Message = $"{this.MaxLoginAttempts}回以上ログインを試行しています。連続ログインはアカウントのロックを引き起こす可能性がありますので、時間をおいてアプリケーションを終了してから、もう一度お試し下さい。"; - this.LoginCommand?.RaiseCanExecutechanged(); - return; - } - - if (this.userCredential != null) - { - this.Message = "ログイン試行中です..."; - this.isLoginAttempting = true; - this.LoginCommand?.RaiseCanExecutechanged(); - - ISession session = LoginPage.Session; - bool result = await session.Login(this.userCredential); - - if (result) - { - - if (this.isUserCredentialChanged && this.IsStoringCredentialEnable) - { - LoginPage.AccountManager.Save(this.UserCredentialName, this.UserCredentialPassword); - } - - this.Message = $"ログインに成功しました。({session.User.Value?.Nickname}さん)"; - this.IsLogin = true; - this.RaiseLoginSucceeded(); - window?.Close(); - } - else - { - this.Message = "ログインに失敗しました。ユーザー名・パスワードは合っていますか?"; - } - - this.isLoginAttempting = false; - this.LoginCommand?.RaiseCanExecutechanged(); - ++this.LoginAttemptCount; - } - } - ); - - var manager = LoginPage.AccountManager; - - if (manager.IsPasswordSaved) - { - this.userCredential = manager.GetUserCredential(); - this.OnPropertyChanged(); - this.isStoringCredentialEnableField = true; - } - } - - /// - /// イベントを発行 - /// - public void RaiseLoginSucceeded() - { - this.LoginSucceeded?.Invoke(this, EventArgs.Empty); - } - - /// - /// ログインコマンド - /// - public CommandBase LoginCommand { get; private set; } - - /// - /// ログイン状態(フィールド) - /// - private bool isLogin; - - /// - /// ログイン状態を取得 - /// - public bool IsLogin - { - get - { - return this.isLogin; - } - set - { - this.SetProperty(ref this.isLogin, value, nameof(this.IsLogin)); - } - } - - /// - /// 認証情報(プロパティー) - /// - private IUserCredential? userCredential; - - /// - /// 認証情報(ユーザー名) - /// - public string UserCredentialName - { - get - { - return this.userCredential?.Username ?? string.Empty; - } - set - { - this.isUserCredentialChanged = true; - this.SetProperty(ref this.userCredential, AccountManager.GetUserCredential(value, this.userCredential?.Password ?? string.Empty)); - } - } - - /// - /// 認証情報(パスワード) - /// - public string UserCredentialPassword - { - get - { - return this.userCredential?.Password ?? string.Empty; - } - set - { - this.isUserCredentialChanged = true; - this.SetProperty(ref this.userCredential, AccountManager.GetUserCredential(this.userCredential?.Username ?? string.Empty, value)); - } - } - - /// - /// ログイン試行中(フィールド) - /// - private bool isLoginAttempting; - - private bool isStoringCredentialEnableField; - - /// - /// 資格情報を保存するかどうか - /// - public bool IsStoringCredentialEnable { get => this.isStoringCredentialEnableField; set => this.SetProperty(ref this.isStoringCredentialEnableField, value); } - - /// - /// メッセージ(フィールド) - /// - private readonly StringBuilder message = new StringBuilder(); - - /// - /// メッセージ - /// - public string Message - { - get - { - return this.message.ToString(); - } - set - { - this.message.AppendLine(value); - this.OnPropertyChanged(nameof(this.message)); - } - } - - /// - /// ログイン試行回数 - /// - private int LoginAttemptCount { get; set; } = 0; - - /// - /// ログイン最大試行回数 - /// - private readonly int MaxLoginAttempts = 5; - - /// - /// 資格情報が変更されたかどうか - /// - private bool isUserCredentialChanged; - - public event EventHandler? LoginSucceeded; - } - - class OpenLoginBrowserBehavior : Behavior