diff options
49 files changed, 1577 insertions, 258 deletions
@@ -5,44 +5,28 @@ A rofi inspired native client for web services. Currently supported web services: `youtube`, `peertube`, `lbry`, `soundcloud`, `nyaa.si`, `manganelo`, `manganelos`, `mangatown`, `mangakatana`, `mangadex`, `onimanga`, `4chan`, `matrix`, `saucenao`, `hotexamples`, `anilist`, `dramacool` and _others_.\ QuickMedia also supports reading local manga and watching local anime, see [local manga](#local-manga) and [local anime](#local-anime) ## Usage -``` -usage: quickmedia [plugin|] [--no-video] [--upscale-images] [--upscale-images-always] [--dir <directory>] [--instance <instance>] [-e <window>] [--video-max-height <height>] -OPTIONS: - plugin|url The plugin to use. Should be either launcher, 4chan, manga, manganelo, manganelos, mangatown, mangakatana, mangadex, onimanga, local-manga, local-anime, youtube, peertube, lbry, soundcloud, nyaa.si, matrix, saucenao, hotexamples, anilist, dramacool, file-manager or stdin. This can also be a youtube url, youtube channel url or a 4chan thread url - --no-video Only play audio when playing a video. Disabled by default - --upscale-images Upscale low-resolution manga pages using waifu2x-ncnn-vulkan. Disabled by default - --upscale-images-always Upscale manga pages using waifu2x-ncnn-vulkan, no matter what the original image resolution is. Disabled by default - --dir <directory> Set the start directory when using file-manager - --instance <instance> The instance to use for peertube - -e <window-id> Embed QuickMedia into another window - --video-max-height <height> Media plugins will try to select a video source that is this size or smaller -EXAMPLES: - quickmedia - quickmedia --upscale-images-always manganelo - quickmedia https://www.youtube.com/watch?v=jHg91NVHh3s - echo -e "hello\nworld" | quickmedia stdin - tabbed -c -k quickmedia launcher -e -``` +Run `qm --help` to see the usage options. ## Installation -If you are running arch linux then you can install QuickMedia from aur: [https://aur.archlinux.org/packages/quickmedia-git/](https://aur.archlinux.org/packages/quickmedia-git/), otherwise you will need to first install [sibs](https://git.dec05eba.com/sibs/) and then run `./install.sh` as root.\ +If you are running arch linux then you can install QuickMedia from aur: [https://aur.archlinux.org/packages/quickmedia-git/](https://aur.archlinux.org/packages/quickmedia-git/), otherwise you will need to run `./install.sh` as root.\ `tar` and curl needs to be installed to run the `install.sh` script.\ -Installing `lld` (the LLVM linker) can improve compile times.\ There is also an unofficial ebuild for gentoo users. On gentoo you can install QuickMedia by running these commands: ```bash -eselect repository add overlay-from-plan9 git https://gitea.plan9.rocks/9/overlay-from-plan9 +eselect repository add overlay-from-plan9 git https://git.plan9.rocks/cat/overlay-from-plan9 emerge --sync overlay-from-plan9 emerge -av quickmedia ``` ## Dependencies +Meson needs to be installed to run the build script. ### Libraries `libglvnd (LibGL.so)`, `libx11`, `libxrandr`, `libmpv` ### Executables -`curl` +`curl`.\ +`yt-dlp` needs to be installed right now to play youtube videos. This requirement will be removed in the future. ### Fonts `noto-fonts` (when `use_system_fonts` config is not set to `true`) ### Optional `noto-fonts-cjk` needs to be installed to view chinese, japanese and korean characters (when `use_system_fonts` config is not set to `true`).\ -`yt-dlp/youtube-dl` needs to be installed to download soundcloud music, fallback for youtube (for example for age restricted videos) or to play/download xxx videos.\ +`yt-dlp` needs to be installed to download soundcloud music or to play/download xxx videos.\ `notify-send` (which is part of `libnotify`) needs to be installed to show notifications (on Linux and other systems that uses d-bus notification system).\ [automedia](https://git.dec05eba.com/AutoMedia/) needs to be installed when tracking anime/manga with `Ctrl + T`.\ `waifu2x-ncnn-vulkan` needs to be installed when using the `--upscale-images` or `--upscale-images-always` option.\ @@ -105,6 +89,7 @@ Type text and then wait and QuickMedia will automatically search.\ `Home`: Go to the first page.\ `End`: Go to the last page.\ `F`: Toggle between scaling the image to the window size or only down scaling if the image is too large.\ +`B`: Show/hide the bottom progress bar.\ `I`: Switch to scroll view. ### Manga scroll view controls `Arrow up`/`K`: Move up.\ @@ -113,6 +98,7 @@ Type text and then wait and QuickMedia will automatically search.\ `Home`: Go to the first page.\ `End`: Go to the last page.\ `F`: Toggle between scaling the image to the window size or only down scaling if the image is too large.\ +`B`: Show/hide the bottom progress bar.\ `I`: Switch to page view. ### Manga chapters controls `Ctrl+T`: Start tracking the manga after the selected chapter ([AutoMedia](https://git.dec05eba.com/AutoMedia/) needs to be installed). @@ -144,7 +130,9 @@ Type text and then wait and QuickMedia will automatically search.\ `Alt+End`: Select the last room in the room list.\ `Ctrl+P`: Pin the selected message.\ `Ctrl+R`: Navigate to the replied to message.\ -`Ctrl+B`: Navigate to the bottom (the latest message). +`Ctrl+B`: Navigate to the bottom (the latest message).\ +`G`: Start typing an `/encrypt` message, `Esc` to cancel.\ +`Ctrl+G`: Start replying to the selected message with `/encrypt`, `Esc` to cancel. #### Pinned messages page controls `Ctrl+D`: Unpin the selected message.\ `Ctrl+R`: Navigate to the pinned message in the messages tab. @@ -184,16 +172,7 @@ Type text and then wait and QuickMedia will automatically search.\ `Ctrl+Enter`/`Click on save`: Save the file.\ `Esc`/`Click on cancel`: Cancel download. ## Matrix text commands -`/help`: Show a list of valid commands.\ -`/upload`: Bring up the file manager and select a file to upload to the room, `Esc` to cancel.\ -`/join [room]`: Join a room by name or id.\ -`/invite`: Invite a user to the room.\ -`/logout`: Logout.\ -`/leave`: Leave the current room.\ -`/me [text]`: Send a message of type "m.emote".\ -`/react [text]`: React to the selected message (also works if you are replying to a message).\ -`/id`: Show the room id.\ -`/encrypt [text]`: Send a message encrypted with gpg. gpg needs to be installed to do this. Uses the gpg key specified by the user id in your config variable "matrix.gpg_user_id". +`/help`: Show a list of valid commands. ## Matrix gpg encryption Matrix gpg email should use the matrix user id, but in email format. For example: @dec05eba:matrix.org should be created with a gpg with the email dec05eba@matrix.org. ## Config @@ -258,7 +237,8 @@ Cache is stored under `$XDG_CACHE_HOME/quickmedia` or `~/.cache/quickmedia`. ## Tabs [tabbed](https://tools.suckless.org/tabbed/) can be used to put quickmedia windows into tabs. After installing `tabbed`, run `tabbed -c -k quickmedia launcher -e`. ## License -QuickMedia is free software licensed under GPL 3.0, see LICENSE for more details. The emojis are from the twemoji project, licensed under Creative Commons Public License 4.0 (see https://raw.githubusercontent.com/twitter/twemoji/v14.0.2/LICENSE-GRAPHICS). The file and directory images are from https://github.com/L4ki/Breeze-Chameleon-Icons and are licensed under GPL 3.0. +QuickMedia is free software licensed under GPL 3.0, see LICENSE for more details. The emojis are from the twemoji project, licensed under Creative Commons Public License 4.0 (see https://raw.githubusercontent.com/twitter/twemoji/v14.0.2/LICENSE-GRAPHICS). The file and directory images are from https://github.com/L4ki/Breeze-Chameleon-Icons and are licensed under GPL 3.0.\ +`src/plugins/utils/aes.*` are from https://github.com/kokke/tiny-AES-c which are licensed under the Unlicence license. # Screenshots ## Youtube search  diff --git a/Scraping.md b/Scraping.md new file mode 100644 index 0000000..e508073 --- /dev/null +++ b/Scraping.md @@ -0,0 +1 @@ +Websites such as dramacool detect devtool usage. This can be disabled by disabling breakpoints (Ctrl+F8) (in the sources tab in devtools) in chromium. @@ -296,4 +296,17 @@ Youtube community tab. v0.m3u8 doesn't work for some lbry videos (such as https://odysee.com/@MoneroMagazine:9/Privacy-101-w-Luke-Smith:2). Fallback to v1.m3u8 (next track in the master.m3u8 file) in such cases. Use DPMSInfoNotify. Use stb_image_resize2.h -Youtube audio only download if audio stream not available, also for youtube-dl fallback.
\ No newline at end of file +Youtube audio only download if audio stream not available, also for youtube-dl fallback. +Make history (local-manga and others) use relative path to the downloads directory for thumbnails. Otherwise the thumbnails wont show when moving the download directory. +Keep the rooms that we were kicked/banned from so we can still read them and re-read the reason for why we were kicked/banned. Or add a list of historical rooms with leave reason. +Fix youtube broken without yt-dlp. Unable to download video. +Use different font for chinese, japanese and korean (or maybe only for korean). +Include noto font instead of having to install it. Only require installing it for other noto fonts. +Re-design matrix /encrypt. You should press G to switch to encryption mode in the room (save it as a room variable, maybe even in file?) and when you do that the matrix icon beside the chat + will have a padlock in front of it and input text will be orange. + Press G again to switch to regular text mode. + Also encrypt media and maybe even reactions. + Add /gpg command to send your gpg key. Generate it if it doesn't exist in gpg --list-keys for your matrix id. + Change matrix gpg config to enable option instead. + Allow pressing enter on a message with PGP key to import it. + Cache decrypted gpg messages in ram, so they dont have to be decrypted again when closing the room and entering it again. diff --git a/depends/html-parser b/depends/html-parser -Subproject ec0e71c259d5aa8be7b8456509b5617d27742b6 +Subproject 3cf44ec7fba308a4b33b0486545b33e334bed74 diff --git a/depends/html-search b/depends/html-search -Subproject 68989816f43ceda8655c2dbdc0d581971e645fc +Subproject a77706c37d3b6df3049f658dd220b813fe1834b diff --git a/depends/jsoncpp b/depends/jsoncpp -Subproject f23fb32fd9d9c3d01fa67afa0d75f7ff227647e +Subproject 98e28b38ffbae24c0e69e620bf18f8a0c5c6f20 diff --git a/depends/mglpp b/depends/mglpp -Subproject fd15a6947741c997072df8b1ddf93cefc416c83 +Subproject 9634e4fef56016f59d45b0cb530087ab36f0171 diff --git a/depends/rapidjson b/depends/rapidjson -Subproject a42f7cc32be7c1d2100d30098e15ff2661c4a61 +Subproject 49b4e2dc7892f27b56a5a9f058c2c9969609324 diff --git a/example-config.json b/example-config.json index d5e6555..7fcb75d 100644 --- a/example-config.json +++ b/example-config.json @@ -43,7 +43,12 @@ "sort_episodes_by_name": true, // If false, anime is displayed in the way they appear in the directory. // If true, QuickMedia tries to group anime episodes into a folder (visually) with the name of the anime - "auto_group_episodes": true + "auto_group_episodes": true, + // If anime should be searched for recursively, where directories become anime and subdirectories become seasons. + // If false, only take the episodes directly in the "directory" path, useful on a slow harddrive when sort_episodes_by_name is set to true + // and the directory is for directly downloaded episodes without a structure. + // Setting this to false is ideal when the directory is the download directory for AutoMedia. + "recursive": true }, "youtube": { // If true, resume playback where you left off in youtube videos @@ -147,6 +152,8 @@ "use_system_fonts": false, // Use system (~/.config/mpv) mpv.conf instead of the one provided by QuickMedia "use_system_mpv_config": false, + // Which mpv profile to use when |use_system_mpv_config| is set to true + "system_mpv_profile": "", // Enable shaders. Only set to false if you have a very slow computer or dont care about graphical effects // such as rounded corners. Rounded corners can also be disabled with the theme option "rounded_rectangles" "enable_shaders": true, diff --git a/include/AsyncImageLoader.hpp b/include/AsyncImageLoader.hpp index 672e0e6..7eed39e 100644 --- a/include/AsyncImageLoader.hpp +++ b/include/AsyncImageLoader.hpp @@ -38,6 +38,7 @@ namespace QuickMedia { mgl::vec2i resize_target_size; }; + bool ffmpeg_convert_image_format(const Path &thumbnail_path, const Path &destination_path); // If |symlink_if_no_resize| is false then a copy is made from |thumbnail_path| to |thumbnail_path_resized| instead of a symlink if |thumbnail_path| is not larger than |resize_target_size|. // One example of why you might not want a symlink is if |thumbnail_path| is a temporary file. bool create_thumbnail(const Path &thumbnail_path, const Path &thumbnail_path_resized, mgl::vec2i resize_target_size, ContentType content_type); diff --git a/include/Config.hpp b/include/Config.hpp index 68e4462..fe210a6 100644 --- a/include/Config.hpp +++ b/include/Config.hpp @@ -48,6 +48,7 @@ namespace QuickMedia { bool sort_by_name = false; // Series bool sort_episodes_by_name = true; bool auto_group_episodes = true; + bool recursive = true; }; struct YoutubeSponsorblock { @@ -131,6 +132,7 @@ namespace QuickMedia { FileManagerConfig file_manager; bool use_system_fonts = false; bool use_system_mpv_config = false; + std::string system_mpv_profile; bool enable_shaders = true; std::string theme = "default"; float scale = 1.0f; @@ -139,5 +141,5 @@ namespace QuickMedia { bool low_latency_mode = false; }; - const Config& get_config(); + Config& get_config(); }
\ No newline at end of file diff --git a/include/Entry.hpp b/include/Entry.hpp index f4cbc5d..54a5713 100644 --- a/include/Entry.hpp +++ b/include/Entry.hpp @@ -32,6 +32,7 @@ namespace QuickMedia { void replace(size_t start_index, size_t length, const std::string &insert_str); int get_caret_index() const; + void set_caret_index(int index); bool is_editable() const; float get_height(); diff --git a/include/ImageViewer.hpp b/include/ImageViewer.hpp index b587b82..b3b18dc 100644 --- a/include/ImageViewer.hpp +++ b/include/ImageViewer.hpp @@ -104,5 +104,7 @@ namespace QuickMedia { mgl::vec2d prev_size_first_page; mgl::vec2d prev_size_last_page; + + bool show_progress_bar = true; }; }
\ No newline at end of file diff --git a/include/QuickMedia.hpp b/include/QuickMedia.hpp index a6de75c..c316f5e 100644 --- a/include/QuickMedia.hpp +++ b/include/QuickMedia.hpp @@ -119,7 +119,7 @@ namespace QuickMedia { void set_clipboard(const std::string &str); bool youtube_dl_extract_url(const std::string &url, std::string &video_url, std::string &audio_url); private: - void init(mgl::WindowHandle parent_window, std::string &program_path, bool no_dialog); + void init(mgl::WindowHandle parent_window, std::string &program_path, bool no_dialog, const std::string &theme); const char* get_youtube_dl_program_name(); void check_youtube_dl_installed(const std::string &plugin_name); void load_plugin_by_name(std::vector<Tab> &tabs, int &start_tab_index, FileManagerMimeType fm_mime_type, FileSelectionHandler file_selection_handler, std::string instance); @@ -245,5 +245,6 @@ namespace QuickMedia { std::mutex login_inputs_mutex; const char *yt_dl_name = nullptr; bool yt_dl_name_checked = false; + bool show_manga_bottom_bar = true; }; } diff --git a/include/Storage.hpp b/include/Storage.hpp index 19dd345..86c34d2 100644 --- a/include/Storage.hpp +++ b/include/Storage.hpp @@ -20,8 +20,8 @@ namespace QuickMedia { DESC }; - // Return false to stop the iterator - using FileIteratorCallback = std::function<bool(const Path &filepath, FileType file_type)>; + // Return false to stop the iterator. + using FileIteratorCallback = std::function<bool(const Path &filepath, FileType file_type, time_t last_modified_seconds)>; Path get_home_dir(); Path get_storage_dir(); @@ -34,8 +34,11 @@ namespace QuickMedia { bool file_get_last_modified_time_seconds(const char *path, time_t *result); int file_overwrite(const Path &path, const std::string &data); int file_overwrite_atomic(const Path &path, const std::string &data); + bool file_append(const Path &path, const std::string &data); + // The callback is called with 0 as the argument (last_modified_seconds) void for_files_in_dir(const Path &path, FileIteratorCallback callback); void for_files_in_dir_sort_last_modified(const Path &path, FileIteratorCallback callback, FileSortDirection sort_dir = FileSortDirection::ASC); + // The callback is called with 0 as the argument (last_modified_seconds) void for_files_in_dir_sort_name(const Path &path, FileIteratorCallback callback, FileSortDirection sort_dir = FileSortDirection::ASC); bool read_file_as_json(const Path &filepath, Json::Value &result); diff --git a/include/Text.hpp b/include/Text.hpp index 89aba99..21b6dc0 100644 --- a/include/Text.hpp +++ b/include/Text.hpp @@ -117,6 +117,7 @@ namespace QuickMedia void replace(size_t start_index, size_t length, const std::string &insert_str); int getCaretIndex() const; + void setCaretIndex(int index); int getNumLines() const; void set_color(mgl::Color color, bool force_color = false); diff --git a/include/Utils.hpp b/include/Utils.hpp index fe0e583..370311a 100644 --- a/include/Utils.hpp +++ b/include/Utils.hpp @@ -8,7 +8,7 @@ namespace QuickMedia { void show_virtual_keyboard(); void hide_virtual_keyboard(); bool is_touch_enabled(); - bool is_running_wayland(); + bool is_running_wayland(void *dpy); time_t iso_utc_to_unix_time(const char *time_str); std::string unix_time_to_local_time_str(time_t unix_time); int64_t get_boottime_milliseconds(); diff --git a/include/VideoPlayer.hpp b/include/VideoPlayer.hpp index bea03fc..ea5996a 100644 --- a/include/VideoPlayer.hpp +++ b/include/VideoPlayer.hpp @@ -39,6 +39,7 @@ namespace QuickMedia { mgl::WindowHandle parent_window; bool no_video = false; bool use_system_mpv_config = false; + std::string system_mpv_profile; bool use_system_input_config = false; // |use_system_mpv_config| has to be true if this is set to true bool keep_open = false; bool resume = false; @@ -5,35 +5,9 @@ cd "$script_dir" [ $(id -u) -ne 0 ] && echo "You need root privileges to run the install script" && exit 1 -curl -sfL 'https://dec05eba.com/files/twemoji.tar.gz' -o /tmp/twemoji.tar.gz -mkdir -p /usr/share/quickmedia/emoji -tar xf /tmp/twemoji.tar.gz --directory=/usr/share/quickmedia/emoji -rm -f /tmp/twemoji.tar.gz +rm -rf build +meson setup build +meson configure --prefix=/usr --buildtype=release -Dstrip=true build +ninja -C build install -sibs build --release video_player -sibs build --release - -install -Dm755 "video_player/sibs-build/$(sibs platform)/release/quickmedia-video-player" "/usr/bin/quickmedia-video-player" -install -Dm755 "sibs-build/$(sibs platform)/release/quickmedia" "/usr/bin/quickmedia" -ln -sf "/usr/bin/quickmedia" "/usr/bin/qm" -install -Dm644 boards.json "/usr/share/quickmedia/boards.json" - -install -Dm644 example-config.json "/usr/share/quickmedia/example-config.json" -install -Dm644 README.md "/usr/share/quickmedia/README.md" - -install -Dm644 mpv/fonts/Material-Design-Iconic-Font.ttf "/usr/share/quickmedia/mpv/fonts/Material-Design-Iconic-Font.ttf" -install -Dm644 mpv/scripts/mordenx.lua "/usr/share/quickmedia/mpv/scripts/mordenx.lua" -install -Dm644 mpv/scripts/ytdl_hook.lua "/usr/share/quickmedia/mpv/scripts/ytdl_hook.lua" -install -Dm644 mpv/input.conf "/usr/share/quickmedia/mpv/input.conf" -install -Dm644 mpv/mpv.conf "/usr/share/quickmedia/mpv/mpv.conf" - -for file in images/* icons/* shaders/* themes/*; do - install -Dm644 "$file" "/usr/share/quickmedia/$file" -done - -for file in launcher/*; do - filename=$(basename "$file") - install -Dm644 "$file" "/usr/share/applications/$filename" -done - -echo "Successfully installed QuickMedia" +echo "Successfully installed quickmedia"
\ No newline at end of file diff --git a/meson.build b/meson.build new file mode 100644 index 0000000..a675ac8 --- /dev/null +++ b/meson.build @@ -0,0 +1,138 @@ +project('quickmedia', ['c', 'cpp'], version : '1.0.0', default_options : ['warning_level=2', 'cpp_std=c++17'], subproject_dir : 'depends') + +if get_option('buildtype') == 'debug' + add_project_arguments('-g3', language : ['c', 'cpp']) +elif get_option('buildtype') == 'release' + add_project_arguments('-DNDEBUG', language : ['c', 'cpp']) +endif + +src = [ + 'external/hash-library/sha256.cpp', + 'generated/Tlds.cpp', + 'generated/Emoji.cpp', + 'src/plugins/utils/aes.c', + 'src/plugins/Fourchan.cpp', + 'src/plugins/Mangadex.cpp', + 'src/plugins/ImageBoard.cpp', + 'src/plugins/MangaCombined.cpp', + 'src/plugins/Manga.cpp', + 'src/plugins/Lbry.cpp', + 'src/plugins/NyaaSi.cpp', + 'src/plugins/HotExamples.cpp', + 'src/plugins/FileManager.cpp', + 'src/plugins/MediaGeneric.cpp', + 'src/plugins/Matrix.cpp', + 'src/plugins/Plugin.cpp', + 'src/plugins/MangaGeneric.cpp', + 'src/plugins/Pipe.cpp', + 'src/plugins/Manganelo.cpp', + 'src/plugins/LocalManga.cpp', + 'src/plugins/Page.cpp', + 'src/plugins/Peertube.cpp', + 'src/plugins/LocalAnime.cpp', + 'src/plugins/AniList.cpp', + 'src/plugins/DramaCool.cpp', + 'src/plugins/utils/UniqueProcess.cpp', + 'src/plugins/utils/WatchProgress.cpp', + 'src/plugins/utils/EpisodeNameParser.cpp', + 'src/plugins/Info.cpp', + 'src/plugins/Youtube.cpp', + 'src/plugins/Saucenao.cpp', + 'src/plugins/Soundcloud.cpp', + 'src/Theme.cpp', + 'src/Storage.cpp', + 'src/Text.cpp', + 'src/Config.cpp', + 'src/DownloadUtils.cpp', + 'src/Json.cpp', + 'src/gui/Button.cpp', + 'src/SearchBar.cpp', + 'src/RoundedRectangle.cpp', + 'src/Entry.cpp', + 'src/Notification.cpp', + 'src/AsyncImageLoader.cpp', + 'src/ImageViewer.cpp', + 'src/Body.cpp', + 'src/Program.cpp', + 'src/main.cpp', + 'src/ImageUtils.cpp', + 'src/NetUtils.cpp', + 'src/BodyItem.cpp', + 'src/Downloader.cpp', + 'src/ResourceLoader.cpp', + 'src/VideoPlayer.cpp', + 'src/Tabs.cpp', + 'src/FileAnalyzer.cpp', + 'src/QuickMedia.cpp', + 'src/M3U8.cpp', + 'src/Utils.cpp', + 'src/StringUtils.cpp', +] + +mglpp_proj = subproject('mglpp') +mglpp_dep = mglpp_proj.get_variable('mglpp_dep') + +jsoncpp_proj = subproject('jsoncpp') +jsoncpp_dep = jsoncpp_proj.get_variable('jsoncpp_dep') + +rapidjson_proj = subproject('rapidjson') +rapidjson_dep = rapidjson_proj.get_variable('rapidjson_dep') + +html_parser_proj = subproject('html-parser') +html_parser_dep = html_parser_proj.get_variable('html_parser_dep') + +html_search_proj = subproject('html-search') +html_search_dep = html_search_proj.get_variable('html_search_dep') + +prefix = get_option('prefix') +datadir = get_option('datadir') +qm_resources_path = join_paths(prefix, datadir, 'quickmedia') + +if get_option('native_arch') == true + add_project_arguments('-march=native', language: ['c', 'cpp']) +endif + +executable( + meson.project_name(), + src, + install : true, + dependencies : [ + mglpp_dep, + jsoncpp_dep, + rapidjson_dep, + html_parser_dep, + html_search_dep, + dependency('threads'), + ], +) + +executable( + 'quickmedia-video-player', + [ + 'video_player/src/main.cpp' + ], + install : true, + dependencies : [ + jsoncpp_dep, + dependency('mpv'), + ], +) + +install_subdir('mpv', install_dir : qm_resources_path) +install_subdir('images', install_dir : qm_resources_path) +install_subdir('icons', install_dir : qm_resources_path) +install_subdir('shaders', install_dir : qm_resources_path) +install_subdir('themes', install_dir : qm_resources_path) + +install_data('launcher/QuickMedia.desktop', install_dir : join_paths(prefix, datadir, 'applications')) +install_data(files('boards.json'), install_dir : qm_resources_path) +install_data(files('example-config.json'), install_dir : qm_resources_path) +install_data(files('README.md'), install_dir : qm_resources_path) + +if get_option('install_symlink') == true + install_symlink('qm', install_dir : join_paths(prefix, 'bin'), pointing_to: join_paths(prefix, 'bin', meson.project_name())) +endif + +if get_option('install_emoji') == true + meson.add_install_script('meson_post_install.sh') +endif diff --git a/meson_options.txt b/meson_options.txt new file mode 100644 index 0000000..0232def --- /dev/null +++ b/meson_options.txt @@ -0,0 +1,3 @@ +option('native_arch', type : 'boolean', value : true, description : 'Build specifically for your own cpu for best performance') +option('install_symlink', type : 'boolean', value : true, description : 'Install a symlink for qm to quickmedia') +option('install_emoji', type : 'boolean', value : true, description : 'Download and install emoji') diff --git a/meson_post_install.sh b/meson_post_install.sh new file mode 100755 index 0000000..1c87f9f --- /dev/null +++ b/meson_post_install.sh @@ -0,0 +1,6 @@ +#!/bin/sh + +curl -sfL 'https://dec05eba.com/files/twemoji.tar.gz' -o /tmp/twemoji.tar.gz +mkdir -p "${MESON_INSTALL_DESTDIR_PREFIX}/share/quickmedia/emoji" +tar xf /tmp/twemoji.tar.gz --directory="${MESON_INSTALL_DESTDIR_PREFIX}/share/quickmedia/emoji" +rm -f /tmp/twemoji.tar.gz
\ No newline at end of file diff --git a/plugins/LocalAnime.hpp b/plugins/LocalAnime.hpp index 708cf45..7ab4119 100644 --- a/plugins/LocalAnime.hpp +++ b/plugins/LocalAnime.hpp @@ -12,6 +12,7 @@ namespace QuickMedia { using LocalAnimeItem = std::variant<LocalAnime, LocalAnimeSeason, LocalAnimeEpisode>; struct LocalAnimeEpisode { + std::string name; Path path; time_t modified_time_seconds = 0; }; diff --git a/plugins/Matrix.hpp b/plugins/Matrix.hpp index a3ce633..f8b10ae 100644 --- a/plugins/Matrix.hpp +++ b/plugins/Matrix.hpp @@ -18,7 +18,7 @@ namespace QuickMedia { struct RoomData; struct Message; - static const int AUTHOR_MAX_LENGTH = 48; + static const int AUTHOR_MAX_LENGTH = 192; class Matrix; @@ -320,7 +320,7 @@ namespace QuickMedia { virtual ~MatrixDelegate() = default; virtual void join_room(RoomData *room) = 0; - virtual void leave_room(RoomData *room, LeaveType leave_type, const std::string &reason) = 0; + virtual void leave_room(RoomData *room, const std::string &event_id, LeaveType leave_type, const std::string &reason) = 0; // Note: calling |room| methods inside this function is not allowed virtual void room_add_tag(RoomData *room, const std::string &tag) = 0; @@ -356,7 +356,7 @@ namespace QuickMedia { MatrixQuickMedia(Program *program, Matrix *matrix, MatrixRoomsPage *rooms_page, MatrixRoomTagsPage *room_tags_page, MatrixInvitesPage *invites_page, MatrixNotificationsPage *notifications_page); void join_room(RoomData *room) override; - void leave_room(RoomData *room, LeaveType leave_type, const std::string &reason) override; + void leave_room(RoomData *room, const std::string &event_id, LeaveType leave_type, const std::string &reason) override; void room_add_tag(RoomData *room, const std::string &tag) override; void room_remove_tag(RoomData *room, const std::string &tag) override; void room_add_new_messages(RoomData *room, const Messages &messages, bool is_initial_sync, MessageDirection message_dir) override; @@ -548,8 +548,9 @@ namespace QuickMedia { class MatrixVideoPage : public VideoPage { public: MatrixVideoPage(Program *program, std::string filename) : VideoPage(program, ""), filename(std::move(filename)) {} - const char* get_title() const override { return ""; } - std::string get_filename() override { return filename; } + const char* get_title() const override; + std::string get_filename() override; + std::string get_download_url_for_clipboard(int max_height) override; private: std::string filename; }; @@ -759,6 +760,11 @@ namespace QuickMedia { MatrixDelegate* get_delegate(); + bool is_another_instance_running() const { return matrix_instance_already_running; } + + void mark_other_notification_as_read(const std::string &event_id); + bool is_other_notification_read(const std::string &event_id) const; + // Calls the |MatrixDelegate| pending events. // Should be called from the main (ui) thread void update(); @@ -783,6 +789,7 @@ namespace QuickMedia { void add_new_invites(); void parse_custom_emoji(const rapidjson::Value &custom_emoji_json); void load_custom_emoji_from_cache(); + void load_other_notifications(); PluginResult get_previous_room_messages(RoomData *room_data, bool latest_messages, size_t &num_new_messages, bool *reached_end = nullptr); void events_add_user_info(const rapidjson::Value &events_json, RoomData *room_data, int64_t timestamp); std::shared_ptr<UserInfo> parse_user_info(const rapidjson::Value &json, const std::string &user_id, RoomData *room_data, int64_t timestamp); @@ -850,6 +857,7 @@ namespace QuickMedia { std::unordered_map<std::string, CustomEmoji> custom_emoji_by_key; std::unordered_set<std::string> silenced_invites; std::unordered_map<std::string, int64_t> qm_read_markers_by_room_cache; + std::unordered_set<std::string> other_notifications_read; MessageQueue<std::shared_ptr<MatrixChatBodyDecryptJob>> decrypt_task; std::thread decrypt_thread; diff --git a/plugins/Page.hpp b/plugins/Page.hpp index 044759a..9e844b3 100644 --- a/plugins/Page.hpp +++ b/plugins/Page.hpp @@ -165,6 +165,7 @@ namespace QuickMedia { virtual void set_url(std::string new_url) { url = std::move(new_url); } std::string get_url() { return url; } virtual std::string get_download_url(int max_height) { (void)max_height; return url; } + virtual std::string get_download_url_for_clipboard(int max_height) { return get_download_url(max_height); } // Returns empty string for no timestamp or if the video doesn't support timestamps. // Timestamp is in seconds. virtual std::string get_url_timestamp() { return ""; } diff --git a/project.conf b/project.conf index b737123..5f01694 100644 --- a/project.conf +++ b/project.conf @@ -7,7 +7,7 @@ platforms = ["posix"] [config] # This needs to be commented out for now because rapidjson depends on undefined behavior according to gcc... #error_on_warning = "true" -ignore_dirs = ["video_player"] +ignore_dirs = ["video_player", "build"] [lang.cpp] version = "c++17" diff --git a/src/AsyncImageLoader.cpp b/src/AsyncImageLoader.cpp index 1118ee6..a6706f5 100644 --- a/src/AsyncImageLoader.cpp +++ b/src/AsyncImageLoader.cpp @@ -26,7 +26,8 @@ #pragma GCC diagnostic pop namespace QuickMedia { - static bool ffmpeg_image_to_png(const Path &thumbnail_path, const Path &destination_path) { + bool ffmpeg_convert_image_format(const Path &thumbnail_path, const Path &destination_path) { + // TODO: Dont output as jpg, ffmpeg jpg is very slow const char *args[] = { "ffmpeg", "-y", "-v", "quiet", "-i", thumbnail_path.data.c_str(), "--", destination_path.data.c_str(), nullptr}; return exec_program(args, nullptr, nullptr) == 0; } @@ -37,7 +38,7 @@ namespace QuickMedia { if(content_type == ContentType::IMAGE_WEBP || content_type == ContentType::IMAGE_AVIF) { Path result_path_tmp = thumbnail_path_resized; result_path_tmp.append(".tmp.png"); - if(!ffmpeg_image_to_png(thumbnail_path, result_path_tmp)) + if(!ffmpeg_convert_image_format(thumbnail_path, result_path_tmp)) return false; input_path = std::move(result_path_tmp); } diff --git a/src/Config.cpp b/src/Config.cpp index 5d16083..64d5019 100644 --- a/src/Config.cpp +++ b/src/Config.cpp @@ -275,6 +275,7 @@ namespace QuickMedia { get_json_value(local_anime_json, "sort_by_name", config->local_anime.sort_by_name); get_json_value(local_anime_json, "sort_episodes_by_name", config->local_anime.sort_episodes_by_name); get_json_value(local_anime_json, "auto_group_episodes", config->local_anime.auto_group_episodes); + get_json_value(local_anime_json, "recursive", config->local_anime.recursive); } const Json::Value &youtube_json = json_root["youtube"]; @@ -405,6 +406,7 @@ namespace QuickMedia { get_json_value(json_root, "use_system_fonts", config->use_system_fonts); get_json_value(json_root, "use_system_mpv_config", config->use_system_mpv_config); + get_json_value(json_root, "system_mpv_profile", config->system_mpv_profile); get_json_value(json_root, "enable_shaders", config->enable_shaders); get_json_value(json_root, "theme", config->theme); get_json_value(json_root, "scale", config->scale); @@ -413,7 +415,7 @@ namespace QuickMedia { get_json_value(json_root, "low_latency_mode", config->low_latency_mode); } - const Config& get_config() { + Config& get_config() { init_config(); return *config; } diff --git a/src/Entry.cpp b/src/Entry.cpp index 5416724..8aa2bba 100644 --- a/src/Entry.cpp +++ b/src/Entry.cpp @@ -107,6 +107,10 @@ namespace QuickMedia { return text.getCaretIndex(); } + void Entry::set_caret_index(int index) { + text.setCaretIndex(index); + } + void Entry::set_position(const mgl::vec2f &pos) { background.set_position(pos); text.set_position(pos + mgl::vec2f(background_margin_horizontal * padding_scale, background_margin_vertical * padding_scale - (float)text.get_character_size() * 0.3f).floor()); diff --git a/src/FileAnalyzer.cpp b/src/FileAnalyzer.cpp index 4deb8c3..1d24c5f 100644 --- a/src/FileAnalyzer.cpp +++ b/src/FileAnalyzer.cpp @@ -157,6 +157,7 @@ namespace QuickMedia { char size_arg_str[512]; snprintf(size_arg_str, sizeof(size_arg_str), "scale=%d:%d:force_original_aspect_ratio=decrease", width, height); + // TODO: Dont output as jpg, ffmpeg jpg is very slow const char *program_args[] = { "ffmpeg", "-y", "-v", "quiet", "-ss", seconds_str, "-i", file.get_filepath().c_str(), "-frames:v", "1", "-vf", size_arg_str, "--", destination_path_tmp.data.c_str(), nullptr }; if(exec_program(program_args, nullptr, nullptr, allowed_exit_status, 2) != 0) { fprintf(stderr, "Failed to execute ffmpeg, maybe its not installed?\n"); @@ -168,6 +169,7 @@ namespace QuickMedia { if(fallback_first_frame && get_file_type(destination_path_tmp) == FileType::FILE_NOT_FOUND) return video_get_frame(file, destination_path, width, height, 0, false); } else { + // TODO: Dont output as jpg, ffmpeg jpg is very slow const char *program_args[] = { "ffmpeg", "-y", "-v", "quiet", "-ss", seconds_str, "-i", file.get_filepath().c_str(), "-frames:v", "1", "--", destination_path_tmp.data.c_str(), nullptr }; if(exec_program(program_args, nullptr, nullptr, allowed_exit_status, 2) != 0) { fprintf(stderr, "Failed to execute ffmpeg, maybe its not installed?\n"); diff --git a/src/ImageViewer.cpp b/src/ImageViewer.cpp index fb9912c..454f34b 100644 --- a/src/ImageViewer.cpp +++ b/src/ImageViewer.cpp @@ -296,6 +296,9 @@ namespace QuickMedia { if(event.key.code == mgl::Keyboard::F) *fit_image_to_window = !*fit_image_to_window; + + if(event.key.code == mgl::Keyboard::B) + show_progress_bar = !show_progress_bar; } else if(event.type == mgl::Event::KeyReleased) { if(is_key_scroll_up(event.key)) up_pressed = false; @@ -426,16 +429,18 @@ namespace QuickMedia { const float font_height = page_text_character_size + 8.0f; const float background_height = font_height + 6.0f; - mgl::Rectangle page_text_background(mgl::vec2f(window_size.x, background_height)); - mgl::Color text_background_color = get_theme().shade_color; - text_background_color.a = 225; - page_text_background.set_color(text_background_color); - page_text_background.set_position(mgl::vec2f(0.0f, window_size.y - background_height)); - window->draw(page_text_background); - - auto page_text_bounds = page_text.get_bounds(); - page_text.set_position(mgl::vec2f(floor(window_size.x * 0.5f - page_text_bounds.size.x * 0.5f), floor(window_size.y - background_height * 0.5f - font_height * 0.55f))); - window->draw(page_text); + if(show_progress_bar) { + mgl::Rectangle page_text_background(mgl::vec2f(window_size.x, background_height)); + mgl::Color text_background_color = get_theme().shade_color; + text_background_color.a = 225; + page_text_background.set_color(text_background_color); + page_text_background.set_position(mgl::vec2f(0.0f, window_size.y - background_height)); + window->draw(page_text_background); + + auto page_text_bounds = page_text.get_bounds(); + page_text.set_position(mgl::vec2f(floor(window_size.x * 0.5f - page_text_bounds.size.x * 0.5f), floor(window_size.y - background_height * 0.5f - font_height * 0.55f))); + window->draw(page_text); + } // Free pages that are not visible on the screen int i = 0; diff --git a/src/QuickMedia.cpp b/src/QuickMedia.cpp index 648f231..d702fc9 100644 --- a/src/QuickMedia.cpp +++ b/src/QuickMedia.cpp @@ -339,7 +339,7 @@ namespace QuickMedia { } static void usage() { - fprintf(stderr, "usage: quickmedia [plugin|url] [--no-video] [--upscale-images] [--upscale-images-always] [--dir <directory>] [--instance <instance>] [-e <window>] [--video-max-height <height>]\n"); + fprintf(stderr, "usage: quickmedia [plugin|url] [--no-video] [--upscale-images] [--upscale-images-always] [--dir <directory>] [--instance <instance>] [-e <window>] [--video-max-height <height>] [--theme <theme>]\n"); fprintf(stderr, "OPTIONS:\n"); fprintf(stderr, " plugin|url The plugin to use. Should be either launcher, 4chan, manga, manganelo, manganelos, mangatown, mangakatana, mangadex, onimanga, local-manga, local-anime, youtube, peertube, lbry, soundcloud, nyaa.si, matrix, saucenao, hotexamples, anilist, dramacool, file-manager, stdin, pornhub, spankbang, xvideos or xhamster. This can also be a youtube url, youtube channel url or a 4chan thread url\n"); fprintf(stderr, " --no-video Only play audio when playing a video. Disabled by default\n"); @@ -349,6 +349,7 @@ namespace QuickMedia { fprintf(stderr, " --instance <instance> The instance to use for peertube\n"); fprintf(stderr, " -e <window-id> Embed QuickMedia into another window\n"); fprintf(stderr, " --video-max-height <height> Media plugins will try to select a video source that is this size or smaller\n"); + fprintf(stderr, " --theme The theme to use. If this is specified then it overrides the theme selected in the QuickMedia config file. Optional.\n"); fprintf(stderr, "EXAMPLES:\n"); fprintf(stderr, " quickmedia\n"); fprintf(stderr, " quickmedia --upscale-images-always manganelo\n"); @@ -409,6 +410,7 @@ namespace QuickMedia { std::string program_path = Path(argv[0]).parent().data; std::string instance; std::string download_filename; + std::string theme; bool no_dialog = false; for(int i = 1; i < argc; ++i) { @@ -523,6 +525,15 @@ namespace QuickMedia { } } else if(strcmp(argv[i], "--no-dialog") == 0) { no_dialog = true; + } else if(strcmp(argv[i], "--theme") == 0) { + if(i < argc - 1) { + theme = argv[i + 1]; + ++i; + } else { + fprintf(stderr, "Missing theme after --theme argument\n"); + usage(); + return -1; + } } else if(strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) { usage(); return 0; @@ -602,7 +613,7 @@ namespace QuickMedia { }; no_video = force_no_video; - init(parent_window, program_path, no_dialog); + init(parent_window, program_path, no_dialog, theme); if(strcmp(plugin_name, "download") == 0) { if(!url) { @@ -687,7 +698,7 @@ namespace QuickMedia { return focused_monitor_center; } - void Program::init(mgl::WindowHandle parent_window, std::string &program_path, bool no_dialog) { + void Program::init(mgl::WindowHandle parent_window, std::string &program_path, bool no_dialog, const std::string &theme) { disp = XOpenDisplay(NULL); if (!disp) { show_notification("QuickMedia", "Failed to open display to X11 server", Urgency::CRITICAL); @@ -699,6 +710,9 @@ namespace QuickMedia { // Initialize config and theme early to prevent possible race condition on initialize get_config(); + if(!theme.empty()) + get_config().theme = theme; + get_theme(); mgl::vec2i monitor_size; @@ -953,7 +967,7 @@ namespace QuickMedia { string_split(urls_str, ',', [&arrays](const char *str, size_t size) { std::string url(str, size); url = strip(url); - if(!url.empty() && (arrays.empty() || arrays.back() != url)) + if(url.find(".com") != std::string::npos && (arrays.empty() || arrays.back() != url)) arrays.push_back(std::move(url)); return true; }); @@ -1095,6 +1109,8 @@ namespace QuickMedia { if(!youtube_dl_name) return false; + const int video_max_height = video_get_max_height(); + std::string ytdl_format; if(no_video) ytdl_format = "(bestaudio/best)"; @@ -1790,7 +1806,7 @@ namespace QuickMedia { // TODO: Remove this once manga history file has been in use for a few months and is filled with history time_t now = time(NULL); - for_files_in_dir_sort_last_modified(content_storage_dir, [&](const Path &filepath, FileType) { + for_files_in_dir_sort_last_modified(content_storage_dir, [&](const Path &filepath, FileType, time_t last_modified_seconds) { // This can happen when QuickMedia crashes/is killed while writing to storage. // In that case, the storage wont be corrupt but there will be .tmp files. // TODO: Remove these .tmp files if they exist during startup @@ -1812,12 +1828,12 @@ namespace QuickMedia { if(!manga_name.isString()) return true; - time_t last_modified_time = 0; - file_get_last_modified_time_seconds(filepath.data.c_str(), &last_modified_time); + if(last_modified_seconds == 0) + file_get_last_modified_time_seconds(filepath.data.c_str(), &last_modified_seconds); // TODO: Add thumbnail auto body_item = BodyItem::create(manga_name.asString()); - body_item->set_description("Last read " + seconds_to_relative_time_str(now - last_modified_time)); + body_item->set_description("Last read " + seconds_to_relative_time_str(now - last_modified_seconds)); body_item->set_description_color(get_theme().faded_text_color); auto thumbnail_it = manga_id_to_thumbnail_url_map.find(manga_id); @@ -3086,7 +3102,10 @@ namespace QuickMedia { } static bool url_should_download_with_youtube_dl(const std::string &url) { - return url.find("pornhub.com") != std::string::npos + return url.find("youtube.com") != std::string::npos + || url.find("youtu.be") != std::string::npos + || url.find("googlevideo.com") != std::string::npos + || url.find("pornhub.com") != std::string::npos || url.find("xhamster.com") != std::string::npos || url.find("spankbang.com") != std::string::npos // TODO: Remove when youtube-dl is no longer required to download soundcloud music @@ -3330,18 +3349,21 @@ namespace QuickMedia { std::string audio_url; bool has_embedded_audio = true; + bool update_duration_retry = false; auto update_video_duration_handler = [&]() { if(!video_player) return; - if(update_duration) { + if(update_duration || update_duration_retry) { update_duration = false; successfully_fetched_video_duration = false; double file_duration = 0.0; video_player->get_duration(&file_duration); video_info.duration = std::max(video_info.duration, file_duration); - if(video_info.duration > 0.001) + if(video_info.duration > 0.001) { + update_duration_retry = false; successfully_fetched_video_duration = true; + } } }; @@ -3361,6 +3383,9 @@ namespace QuickMedia { update_time_pos = true; } + if(update_duration || (update_duration_retry && update_time_pos)) + update_video_duration_handler(); + if(update_time_pos) { update_time_pos = false; const double prev_video_time_pos = video_time_pos; @@ -3382,8 +3407,6 @@ namespace QuickMedia { } } } - - update_video_duration_handler(); }; auto load_video_error_check = [&](std::string start_time = "", bool reuse_media_source = false) mutable { @@ -3510,6 +3533,7 @@ namespace QuickMedia { startup_args.parent_window = window.get_system_handle(); startup_args.no_video = is_audio_only; startup_args.use_system_mpv_config = get_config().use_system_mpv_config || video_page->is_local(); + startup_args.system_mpv_profile = get_config().system_mpv_profile; startup_args.use_system_input_config = video_page->is_local(); startup_args.keep_open = is_matrix && !is_youtube; startup_args.resume = false; @@ -3589,6 +3613,7 @@ namespace QuickMedia { //video_player->set_paused(false); } else if(strcmp(event_name, "start-file") == 0) { update_duration = true; + update_duration_retry = true; added_recommendations = false; time_watched_timer.restart(); video_loaded = true; @@ -3621,7 +3646,7 @@ namespace QuickMedia { mgl::Clock cursor_hide_timer; auto save_video_url_to_clipboard = [this, video_page]() { - std::string url = video_page->get_download_url(video_get_max_height()); + std::string url = video_page->get_download_url_for_clipboard(video_get_max_height()); if(video_url_supports_timestamp(url)) { double time_in_file = 0.0; if(video_player && (video_player->get_time_in_file(&time_in_file) != VideoPlayer::Error::OK)) @@ -4230,6 +4255,15 @@ namespace QuickMedia { } } + { + FileAnalyzer file_analyzer; + if(file_analyzer.load_file(image_filepath_tmp.data.c_str(), false) && file_analyzer.get_content_type() == ContentType::IMAGE_WEBP) { + Path new_filepath = image_filepath_tmp.data + ".png"; + if(ffmpeg_convert_image_format(image_filepath_tmp, new_filepath)) + image_filepath_tmp = std::move(new_filepath); + } + } + bool rename_immediately = true; if(upscale_image_action == UpscaleImageAction::LOW_RESOLUTION) { int screen_width, screen_height; @@ -4476,6 +4510,9 @@ namespace QuickMedia { } else if(event.key.code == mgl::Keyboard::F) { fit_image_to_window = !fit_image_to_window; redraw = true; + } else if(event.key.code == mgl::Keyboard::B) { + show_manga_bottom_bar = !show_manga_bottom_bar; + redraw = true; } } } @@ -4500,7 +4537,7 @@ namespace QuickMedia { } const float font_height = chapter_text_character_size + 8.0f; - const float bottom_panel_height = font_height + 6.0f; + const float bottom_panel_height = show_manga_bottom_bar ? font_height + 6.0f : 0.0f; mgl::vec2f content_size; content_size.x = window_size.x; @@ -4539,13 +4576,15 @@ namespace QuickMedia { window.draw(image); } - chapter_text_background.set_size(mgl::vec2f(window_size.x, bottom_panel_height)); - chapter_text_background.set_position(mgl::vec2f(0.0f, std::floor(window_size.y - bottom_panel_height))); - window.draw(chapter_text_background); + if(show_manga_bottom_bar) { + chapter_text_background.set_size(mgl::vec2f(window_size.x, bottom_panel_height)); + chapter_text_background.set_position(mgl::vec2f(0.0f, std::floor(window_size.y - bottom_panel_height))); + window.draw(chapter_text_background); - auto text_bounds = chapter_text.get_bounds(); - chapter_text.set_position(vec2f_floor(window_size.x * 0.5f - text_bounds.size.x * 0.5f, window_size.y - bottom_panel_height * 0.5f - font_height * 0.55f)); - window.draw(chapter_text); + auto text_bounds = chapter_text.get_bounds(); + chapter_text.set_position(vec2f_floor(window_size.x * 0.5f - text_bounds.size.x * 0.5f, window_size.y - bottom_panel_height * 0.5f - font_height * 0.55f)); + window.draw(chapter_text); + } window.display(); } else { @@ -6280,7 +6319,9 @@ namespace QuickMedia { "/me [text]: Send a message of type \"m.emote\".\n" "/react [text]: React to the selected message (also works if you are replying to a message).\n" "/id: Show the room id.\n" - "/encrypt [text]: Send a message encrypted with gpg. gpg needs to be installed to do this. Uses the gpg key specified by the user id in your config variable \"matrix.gpg_user_id\"."; + "/whoami: Show your user id.\n" + "/encrypt [text]: Send a message encrypted with gpg. gpg needs to be installed to do this. Uses the gpg key specified by the user id in your config variable \"matrix.gpg_user_id\".\n" + "/bot [text]: Send a message as a bot would."; message->timestamp = time(nullptr) * 1000; // TODO: What if the user has broken local time? matrix->append_system_message(current_room, std::move(message)); @@ -6300,9 +6341,24 @@ namespace QuickMedia { chat_state = ChatState::NAVIGATING; matrix->get_room_extra_data(current_room).editing_message_id.clear(); return true; + } else if(text == "/whoami") { + auto message = std::make_shared<Message>(); + message->type = MessageType::SYSTEM; + message->user = me; + message->body = me->user_id; + message->timestamp = time(nullptr) * 1000; // TODO: What if the user has broken local time? + matrix->append_system_message(current_room, std::move(message)); + + chat_input.set_editable(false); + chat_state = ChatState::NAVIGATING; + matrix->get_room_extra_data(current_room).editing_message_id.clear(); + return true; } else if(strncmp(text.c_str(), "/me ", 4) == 0) { msgtype = "m.emote"; text.erase(text.begin(), text.begin() + 4); + } else if(strncmp(text.c_str(), "/bot ", 5) == 0) { + msgtype = "m.notice"; + text.erase(text.begin(), text.begin() + 5); } else if(strncmp(text.c_str(), "/react ", 7) == 0) { msgtype = "m.reaction"; text.erase(text.begin(), text.begin() + 7); @@ -7112,6 +7168,30 @@ namespace QuickMedia { } }; + auto start_typing = [&] { + RoomExtraData &room_extra_data = matrix->get_room_extra_data(current_room); + frame_skip_text_entry = true; + chat_input.set_editable(true); + if(get_config().matrix.clear_message_on_escape || !room_extra_data.editing_message_id.empty()) { + chat_input.set_text(""); + room_extra_data.editing_message_id.clear(); + } + chat_state = ChatState::TYPING_MESSAGE; + }; + + auto start_replying = [&](std::shared_ptr<BodyItem> selected_body_item) { + RoomExtraData &room_extra_data = matrix->get_room_extra_data(current_room); + chat_state = ChatState::REPLYING; + currently_operating_on_item = selected_body_item; + chat_input.set_editable(true); + if(get_config().matrix.clear_message_on_escape || !room_extra_data.editing_message_id.empty()) { + chat_input.set_text(""); + room_extra_data.editing_message_id.clear(); + } + replying_to_text.set_string("Replying to:"); + frame_skip_text_entry = true; + }; + for(size_t i = 0; i < tabs.size(); ++i) { tabs[i].body->on_top_reached = on_top_reached; tabs[i].body->on_bottom_reached = on_bottom_reached; @@ -7258,14 +7338,16 @@ namespace QuickMedia { } if(event.key.code == mgl::Keyboard::I && !event.key.control) { - RoomExtraData &room_extra_data = matrix->get_room_extra_data(current_room); - frame_skip_text_entry = true; - chat_input.set_editable(true); - if(get_config().matrix.clear_message_on_escape || !room_extra_data.editing_message_id.empty()) { - chat_input.set_text(""); - room_extra_data.editing_message_id.clear(); + start_typing(); + } + + if(event.key.code == mgl::Keyboard::G && !event.key.control) { + start_typing(); + std::string chat_text = chat_input.get_text(); + if(!string_starts_with(chat_text, "/encrypt")) { + chat_input.set_text("/encrypt " + chat_text); + chat_input.set_caret_index(chat_input.get_caret_index() + 9); } - chat_state = ChatState::TYPING_MESSAGE; } if(event.key.control && event.key.code == mgl::Keyboard::V) { @@ -7352,21 +7434,27 @@ namespace QuickMedia { // TODO: Show inline notification show_notification("QuickMedia", "You can't reply to a message that hasn't been sent yet"); } else { - RoomExtraData &room_extra_data = matrix->get_room_extra_data(current_room); - chat_state = ChatState::REPLYING; - currently_operating_on_item = selected; - chat_input.set_editable(true); - if(get_config().matrix.clear_message_on_escape || !room_extra_data.editing_message_id.empty()) { - chat_input.set_text(""); - room_extra_data.editing_message_id.clear(); - } - replying_to_text.set_string("Replying to:"); - frame_skip_text_entry = true; + start_replying(std::move(selected)); } } } } + if(event.key.code == mgl::Keyboard::G && event.key.control) { + std::shared_ptr<BodyItem> selected = tabs[selected_tab].body->get_selected_shared(); + if(static_cast<Message*>(selected->userdata)->event_id.empty()) { + // TODO: Show inline notification + show_notification("QuickMedia", "You can't reply to a message that hasn't been sent yet"); + } else { + start_replying(std::move(selected)); + std::string chat_text = chat_input.get_text(); + if(!string_starts_with(chat_text, "/encrypt")) { + chat_input.set_text("/encrypt " + chat_text); + chat_input.set_caret_index(chat_input.get_caret_index() + 9); + } + } + } + if(event.key.code == mgl::Keyboard::B && event.key.control) { // Reload room, goes to latest message l0l move_room = true; @@ -7391,6 +7479,10 @@ namespace QuickMedia { } else { RoomExtraData &room_extra_data = matrix->get_room_extra_data(current_room); std::string body_text_unformatted = Text::to_printable_string(selected->get_description()); + MatrixChatBodyItemData *matrix_chat_body_item = static_cast<MatrixChatBodyItemData*>(selected->extra.get()); + const char *encrypt_prefix = "🔒 "; + if(string_starts_with(body_text_unformatted, encrypt_prefix) && matrix_chat_body_item) + body_text_unformatted.replace(0, strlen(encrypt_prefix), "/encrypt "); chat_state = ChatState::EDITING; currently_operating_on_item = selected; diff --git a/src/RoundedRectangle.cpp b/src/RoundedRectangle.cpp index 82ade02..81cf63b 100644 --- a/src/RoundedRectangle.cpp +++ b/src/RoundedRectangle.cpp @@ -68,6 +68,6 @@ namespace QuickMedia { } } - target.draw(vertices, 4, mgl::PrimitiveType::Quads, rounded_rectangle_shader); + target.draw(vertices, 4, mgl::PrimitiveType::Quads, {0, 0}, rounded_rectangle_shader); } }
\ No newline at end of file diff --git a/src/Storage.cpp b/src/Storage.cpp index dc6b6a2..5c12911 100644 --- a/src/Storage.cpp +++ b/src/Storage.cpp @@ -244,12 +244,27 @@ namespace QuickMedia { return rename_atomic(tmp_path.data.c_str(), path.data.c_str()); } + bool file_append(const Path &path, const std::string &data) { + FILE *file = fopen_eintr(path.data.c_str(), "ab"); + if(!file) { + perror(path.data.c_str()); + return false; + } + + if(fwrite_eintr(data.data(), data.size(), file) != data.size()) { + fclose(file); + return false; + } + + return fclose(file) == 0; + } + void for_files_in_dir(const Path &path, FileIteratorCallback callback) { try { for(auto &p : std::filesystem::directory_iterator(path.data)) { std::error_code ec; const FileType file_type = p.is_directory(ec) ? FileType::DIRECTORY : FileType::REGULAR; - if(!callback(p.path().string(), file_type)) + if(!callback(p.path().string(), file_type, 0)) break; } } catch(const std::filesystem::filesystem_error &err) { @@ -258,19 +273,13 @@ namespace QuickMedia { } } - static std::filesystem::file_time_type file_get_filetime_or(const std::filesystem::directory_entry &path, std::filesystem::file_time_type default_value) { - try { - return path.last_write_time(); - } catch(const std::filesystem::filesystem_error &err) { - return default_value; - } - } - void for_files_in_dir_sort_last_modified(const Path &path, FileIteratorCallback callback, FileSortDirection sort_dir) { - std::vector<std::filesystem::directory_entry> paths; + std::vector<std::pair<std::filesystem::directory_entry, time_t>> paths_with_last_modified; try { for(auto &p : std::filesystem::directory_iterator(path.data)) { - paths.push_back(p); + time_t last_modified = 0; + file_get_last_modified_time_seconds(p.path().c_str(), &last_modified); + paths_with_last_modified.push_back(std::make_pair(p, last_modified)); } } catch(const std::filesystem::filesystem_error &err) { fprintf(stderr, "Failed to list files in directory %s, error: %s\n", path.data.c_str(), err.what()); @@ -278,19 +287,19 @@ namespace QuickMedia { } if(sort_dir == FileSortDirection::ASC) { - std::sort(paths.begin(), paths.end(), [](const std::filesystem::directory_entry &path1, std::filesystem::directory_entry &path2) { - return file_get_filetime_or(path1, std::filesystem::file_time_type::min()) > file_get_filetime_or(path2, std::filesystem::file_time_type::min()); + std::sort(paths_with_last_modified.begin(), paths_with_last_modified.end(), [](const auto &path1, const auto &path2) { + return path1.second > path2.second; }); } else { - std::sort(paths.begin(), paths.end(), [](const std::filesystem::directory_entry &path1, std::filesystem::directory_entry &path2) { - return file_get_filetime_or(path1, std::filesystem::file_time_type::min()) < file_get_filetime_or(path2, std::filesystem::file_time_type::min()); + std::sort(paths_with_last_modified.begin(), paths_with_last_modified.end(), [](const auto &path1, const auto &path2) { + return path1.second < path2.second; }); } - for(auto &p : paths) { + for(auto &p : paths_with_last_modified) { std::error_code ec; - const FileType file_type = p.is_directory(ec) ? FileType::DIRECTORY : FileType::REGULAR; - if(!callback(p.path().string(), file_type)) + const FileType file_type = p.first.is_directory(ec) ? FileType::DIRECTORY : FileType::REGULAR; + if(!callback(p.first.path().string(), file_type, p.second)) break; } } @@ -319,7 +328,7 @@ namespace QuickMedia { for(auto &p : paths) { std::error_code ec; const FileType file_type = p.is_directory(ec) ? FileType::DIRECTORY : FileType::REGULAR; - if(!callback(p.path().string(), file_type)) + if(!callback(p.path().string(), file_type, 0)) break; } } diff --git a/src/Text.cpp b/src/Text.cpp index 2b62798..6333e8e 100644 --- a/src/Text.cpp +++ b/src/Text.cpp @@ -285,6 +285,10 @@ namespace QuickMedia int Text::getCaretIndex() const { return caretIndex; } + + void Text::setCaretIndex(int index) { + caretIndex = index; + } void Text::set_color(mgl::Color color, bool force_color) { diff --git a/src/Utils.cpp b/src/Utils.cpp index ca153ab..e6628d1 100644 --- a/src/Utils.cpp +++ b/src/Utils.cpp @@ -2,13 +2,11 @@ #include <stdlib.h> #include <stdio.h> #include <string.h> -#include <locale.h> +#include <X11/Xlib.h> namespace QuickMedia { static bool qm_enable_touch = false; static bool qm_enable_touch_set = false; - static bool wayland_display_set = false; - static const char *wayland_display = nullptr; void show_virtual_keyboard() { if(!is_touch_enabled()) @@ -38,14 +36,9 @@ namespace QuickMedia { return qm_enable_touch; } - // TODO: Find a better way to detect this. This will return true on ubuntu when running gnome in x11 mode - bool is_running_wayland() { - if(wayland_display_set) - return wayland_display; - - wayland_display = getenv("WAYLAND_DISPLAY"); - wayland_display_set = true; - return wayland_display; + bool is_running_wayland(void *dpy) { + int opcode, event, error; + return XQueryExtension((Display*)dpy, "XWAYLAND", &opcode, &event, &error); } time_t iso_utc_to_unix_time(const char *time_str) { diff --git a/src/VideoPlayer.cpp b/src/VideoPlayer.cpp index f263fc9..ea18289 100644 --- a/src/VideoPlayer.cpp +++ b/src/VideoPlayer.cpp @@ -159,7 +159,7 @@ namespace QuickMedia { fcntl(ipc_socket, F_SETFL, flags | O_NONBLOCK); const std::string ipc_fd = std::to_string(fd[1]); - std::string cache_dir = "--cache-dir=" + std::move(get_cache_dir().join("media").data); + std::string cache_dir = "--demuxer-cache-dir=" + std::move(get_cache_dir().join("media").data); std::string wid_arg = "--wid="; wid_arg += std::to_string(startup_args.parent_window); @@ -177,7 +177,6 @@ namespace QuickMedia { const std::string config_dir = "--config-dir=" + startup_args.resource_root + "mpv"; const std::string input_conf_file = "--input-conf=" + startup_args.resource_root + "mpv/input.conf"; - const std::string ytdl_hook_file = "--scripts=" + startup_args.resource_root + "mpv/scripts/ytdl_hook.lua"; std::vector<const char*> args; // TODO: Resume playback if the last video played matches the first video played next time QuickMedia is launched @@ -218,7 +217,7 @@ namespace QuickMedia { args.push_back("--resume-playback=no"); } - if(is_running_wayland()) { + if(is_running_wayland(display)) { args.push_back("--gpu-context=x11egl"); fprintf(stderr, "Wayland detected. Launching mpv in x11egl mode\n"); } @@ -251,27 +250,51 @@ namespace QuickMedia { if(!startup_args.referer.empty()) args.push_back(referer_arg.c_str()); - std::string mpris_arg; - Path mpris_path = get_config_dir_xdg().join("mpv").join("scripts").join("mpris.so"); - if(get_file_type(mpris_path) == FileType::REGULAR) - mpris_arg = "--scripts=" + mpris_path.data; + std::string scripts; + std::string scripts_arg; + const Path mpris_path = get_config_dir_xdg().join("mpv").join("scripts").join("mpris.so"); + if(get_file_type(mpris_path) == FileType::REGULAR) { + if(!scripts.empty()) + scripts += ":"; + scripts += mpris_path.data; + } + + const Path mpris_system_path = "/etc/mpv/scripts/mpris.so"; + if(get_file_type(mpris_system_path) == FileType::REGULAR) { + if(!scripts.empty()) + scripts += ":"; + scripts += mpris_system_path.data; + } + + std::string profile_arg; if(startup_args.use_system_mpv_config) { + if(!scripts.empty()) + scripts += ":"; + scripts += startup_args.resource_root + "mpv/scripts/ytdl_hook.lua"; + args.push_back("--config=yes"); args.push_back("--load-scripts=yes"); args.push_back("--osc=yes"); args.push_back(input_conf_file.c_str()); - args.push_back(ytdl_hook_file.c_str()); + + if(!startup_args.system_mpv_profile.empty()) + profile_arg = "--profile=" + startup_args.system_mpv_profile; } else { args.insert(args.end(), { config_dir.c_str(), "--config=yes" }); + } - if(!mpris_arg.empty()) - args.push_back(mpris_arg.c_str()); + if(!scripts.empty()) { + scripts_arg = "--scripts=" + scripts; + args.push_back(scripts_arg.c_str()); } + if(!profile_arg.empty()) + args.push_back(profile_arg.c_str()); + std::string force_media_title_arg; if(!startup_args.title.empty()) { force_media_title_arg = "--force-media-title=" + startup_args.title; diff --git a/src/main.cpp b/src/main.cpp index ef0568b..3d675bc 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,8 +1,10 @@ #include "../include/QuickMedia.hpp" #include <locale.h> +#include <malloc.h> int main(int argc, char **argv) { setlocale(LC_ALL, "C"); // Sigh... stupid C + mallopt(M_MMAP_THRESHOLD, 65536); QuickMedia::Program program; return program.run(argc, argv); } diff --git a/src/plugins/DramaCool.cpp b/src/plugins/DramaCool.cpp index de5357a..f513df3 100644 --- a/src/plugins/DramaCool.cpp +++ b/src/plugins/DramaCool.cpp @@ -3,9 +3,25 @@ #include "../../include/StringUtils.hpp" #include "../../include/M3U8.hpp" #include "../../plugins/utils/WatchProgress.hpp" +#include "../../external/cppcodec/base64_rfc4648.hpp" + +#define AES256 1 +#define CBC 1 +#define ECB 0 +#define CTR 0 + +extern "C" { +#include "utils/aes.h" +} + #include <json/value.h> +#include <json/reader.h> #include <quickmedia/HtmlSearch.h> +// Keys are from: https://github.com/henry-richard7/shows-flix/blob/main/lib/Scraper/vidstream_scraper.dart +#define ASIANLOAD_KEY "93422192433952489752342908585752" +#define ASIANLOAD_IV "9262859232435825" + // TODO: Add bookmarks page, history, track watch progress, automatically go to next episode, subscribe, etc. namespace QuickMedia { @@ -125,6 +141,7 @@ namespace QuickMedia { struct VideoSources { //std::string streamsss; + std::string asianload; std::string streamtape; std::string mixdrop; std::string mp4upload; @@ -147,6 +164,7 @@ namespace QuickMedia { static void dembed_extract_video_sources(const std::string &website_data, VideoSources &video_sources) { //dembed_extract_video_source(website_data, "streamsss.net", video_sources.streamsss); + dembed_extract_video_source(website_data, "asianbxkiun.pro/embedplus?id=", video_sources.asianload); dembed_extract_video_source(website_data, "streamtape.com", video_sources.streamtape); dembed_extract_video_source(website_data, "mixdrop.co", video_sources.mixdrop); dembed_extract_video_source(website_data, "www.mp4upload.com", video_sources.mp4upload); @@ -201,6 +219,7 @@ namespace QuickMedia { if(result != DownloadResult::OK) return false; + // TODO: get the resolution that is lower or equal to the height we want url = M3U8Stream::get_highest_resolution_stream(m3u8_get_streams(website_data)).url; return true; } @@ -400,6 +419,173 @@ namespace QuickMedia { return true; } + static std::string url_extract_param(const std::string &url, const std::string ¶m_key) { + std::string value; + const std::string param = param_key + "="; + size_t start_index = url.find(param); + if(start_index == std::string::npos) + return value; + + start_index += param.size(); + size_t end_index = url.find('&', start_index); + if(end_index == std::string::npos) { + end_index = url.find('"', start_index); + if(end_index == std::string::npos) + return value; + } + + value = url.substr(start_index, end_index - start_index); + return value; + } + + static size_t align_up(size_t value, size_t alignment) { + size_t v = value / alignment; + if(value % alignment != 0) + v++; + if(v == 0) + v = 1; + return v * alignment; + } + + // |key| should be a multiple of AES_KEYLEN (32) and |iv| should be a multiple of AES_BLOCKLEN (16) + static std::string aes_cbc_encrypt_base64(const std::string &str, const uint8_t *key, const uint8_t *iv) { + std::string result; + + const size_t input_size = align_up(str.size(), AES_BLOCKLEN); + uint8_t *input = (uint8_t*)malloc(input_size); + if(!input) + return result; + + memcpy(input, str.data(), str.size()); + + // PKCS#7 padding + const int num_padded_bytes = input_size - str.size(); + memset(input + str.size(), num_padded_bytes, num_padded_bytes); + + struct AES_ctx ctx; + AES_init_ctx_iv(&ctx, key, iv); + AES_CBC_encrypt_buffer(&ctx, input, input_size); + + std::string input_data_str((const char*)input, input_size); + result = cppcodec::base64_rfc4648::encode<std::string>(input_data_str); + free(input); + + return result; + } + + static std::string aes_cbc_decrypt(const std::string &str, const uint8_t *key, const uint8_t *iv) { + std::string result; + + const size_t input_size = align_up(str.size(), AES_BLOCKLEN); + uint8_t *input = (uint8_t*)malloc(input_size); + if(!input) + return result; + + memcpy(input, str.data(), str.size()); + + // PKCS#7 padding + const int num_padded_bytes = input_size - str.size(); + memset(input + str.size(), num_padded_bytes, num_padded_bytes); + + struct AES_ctx ctx; + AES_init_ctx_iv(&ctx, key, iv); + AES_CBC_decrypt_buffer(&ctx, input, input_size); + + result.assign((const char*)input, str.size()); + free(input); + + return result; + } + + static std::string asianload_decrypt_response_get_hls_url(const Json::Value &json_result) { + std::string url; + if(!json_result.isObject()) + return url; + + const Json::Value &data_json = json_result["data"]; + if(!data_json.isString()) + return url; + + std::string data_raw = cppcodec::base64_rfc4648::decode<std::string>(data_json.asString()); + const std::string input = aes_cbc_decrypt(data_raw, (const uint8_t*)ASIANLOAD_KEY, (const uint8_t*)ASIANLOAD_IV); + + Json::CharReaderBuilder json_builder; + std::unique_ptr<Json::CharReader> json_reader(json_builder.newCharReader()); + std::string json_errors; + Json::Value result; + if(!json_reader->parse(input.data(), input.data() + data_raw.size(), &result, &json_errors)) { + fprintf(stderr, "asianload_decrypt_response_get_hls_url error: %s\n", json_errors.c_str()); + return url; + } + + if(!result.isObject()) + return url; + + const Json::Value &source_json = result["source"]; + if(!source_json.isArray()) + return url; + + // The json data also contains backup (source_bk) and tracks (vtt), but we ignore those for now + for(const Json::Value &item_json : source_json) { + if(!item_json.isObject()) + continue; + + const Json::Value &file_json = item_json["file"]; + const Json::Value &type_json = item_json["type"]; + if(!file_json.isString() || !type_json.isString()) + continue; + + if(strcmp(type_json.asCString(), "hls") != 0) + continue; + + url = file_json.asString(); + break; + } + + return url; + } + + static std::string hls_url_remove_filename(const std::string &hls_url) { + std::string result; + size_t index = hls_url.rfind('/'); + if(index == std::string::npos) + return result; + result = hls_url.substr(0, index); + return result; + } + + static std::string asianload_get_best_quality_stream(const std::string &hls_url) { + std::string url; + std::string website_data; + DownloadResult result = download_to_string(hls_url, website_data, {}, true); + if(result != DownloadResult::OK) + return url; + + // TODO: get the resolution that is lower or equal to the height we want + url = M3U8Stream::get_highest_resolution_stream(m3u8_get_streams(website_data)).url; + return hls_url_remove_filename(hls_url) + "/" + url; + } + + static void asianload_get_video_url(Page *page, const std::string &asianload_url, std::string &video_url) { + const std::string id = url_extract_param(asianload_url, "id"); + const std::string token = url_extract_param(asianload_url, "token"); + const std::string bla = aes_cbc_encrypt_base64(id, (const uint8_t*)ASIANLOAD_KEY, (const uint8_t*)ASIANLOAD_IV); + + const int64_t expires = (int64_t)time(NULL) + (60LL * 60LL); // current time + 1 hour, in seconds + const std::string url = "https://asianbxkiun.pro/encrypt-ajax.php?id=" + bla + "&token=" + token + "&expires=" + std::to_string(expires) + "&mip=0.0.0.0&refer=https://asianc.sh/&op=2&alias=" + id; + + Json::Value json_result; + DownloadResult result = page->download_json(json_result, url, {{ "-H", "x-requested-with: XMLHttpRequest" }}, true); + if(result != DownloadResult::OK) + return; + + const std::string hls_url = asianload_decrypt_response_get_hls_url(json_result); + if(hls_url.empty()) + return; + + video_url = asianload_get_best_quality_stream(hls_url); + } + PluginResult DramaCoolEpisodesPage::submit(const SubmitArgs &args, std::vector<Tab> &result_tabs) { std::string website_data; DownloadResult result = download_to_string(args.url, website_data, {}, true); @@ -453,6 +639,10 @@ namespace QuickMedia { std::string video_url; std::string referer; + if(!video_sources.asianload.empty() && video_url.empty()) { + asianload_get_video_url(this, video_sources.asianload, video_url); + } + if(!video_sources.streamtape.empty() && video_url.empty()) { result = download_to_string(video_sources.streamtape, website_data, {}, true); if(result == DownloadResult::OK) { diff --git a/src/plugins/LocalAnime.cpp b/src/plugins/LocalAnime.cpp index c906b9b..0febb23 100644 --- a/src/plugins/LocalAnime.cpp +++ b/src/plugins/LocalAnime.cpp @@ -55,7 +55,7 @@ namespace QuickMedia { return season.name; } else if(std::holds_alternative<LocalAnimeEpisode>(item)) { const LocalAnimeEpisode &episode = std::get<LocalAnimeEpisode>(item); - return episode.path.filename(); + return episode.name; } else { return {}; } @@ -139,13 +139,15 @@ namespace QuickMedia { std::vector<LocalAnimeItem> grouped_items; for(LocalAnimeItem &item : items) { if(std::holds_alternative<LocalAnimeEpisode>(item)) { - const LocalAnimeEpisode &episode = std::get<LocalAnimeEpisode>(item); + LocalAnimeEpisode &episode = std::get<LocalAnimeEpisode>(item); std::optional<EpisodeNameParts> name_parts = episode_name_extract_parts(episode.path.filename()); if(!name_parts) { grouped_items.push_back(std::move(item)); continue; } + episode.name = name_parts->episode; + GroupedAnime &grouped_anime = anime_by_name[name_parts->anime]; if(grouped_anime.anime.name.empty()) grouped_anime.anime.name = name_parts->anime; @@ -190,7 +192,7 @@ namespace QuickMedia { static std::vector<LocalAnimeItem> get_episodes_in_directory(const Path &directory) { std::vector<LocalAnimeItem> episodes; - for_files_in_dir_sort_name(directory, [&episodes](const Path &filepath, FileType file_type) -> bool { + for_files_in_dir_sort_name(directory, [&episodes](const Path &filepath, FileType file_type, time_t) -> bool { if(file_type != FileType::REGULAR || !is_video_ext(filepath.ext())) return true; @@ -198,7 +200,7 @@ namespace QuickMedia { if(!file_get_last_modified_time_seconds(filepath.data.c_str(), &modified_time_seconds)) return true; - episodes.push_back(LocalAnimeEpisode{ filepath, modified_time_seconds }); + episodes.push_back(LocalAnimeEpisode{ filepath.data, filepath, modified_time_seconds }); return true; }, FileSortDirection::DESC); return episodes; @@ -206,21 +208,28 @@ namespace QuickMedia { static std::vector<LocalAnimeItem> get_episodes_or_seasons_in_directory(const Path &directory) { std::vector<LocalAnimeItem> anime_items; - auto callback = [&](const Path &filepath, FileType file_type) -> bool { - time_t modified_time_seconds; - if(!file_get_last_modified_time_seconds(filepath.data.c_str(), &modified_time_seconds)) - return true; - + auto callback = [&](const Path &filepath, FileType file_type, time_t last_modified_seconds) -> bool { if(file_type == FileType::REGULAR) { - if(is_video_ext(filepath.ext())) - anime_items.push_back(LocalAnimeEpisode{ filepath, modified_time_seconds }); + if(is_video_ext(filepath.ext())) { + if(last_modified_seconds == 0) { + if(!file_get_last_modified_time_seconds(filepath.data.c_str(), &last_modified_seconds)) + return true; + } + + anime_items.push_back(LocalAnimeEpisode{ filepath.data, filepath, last_modified_seconds }); + } return true; } + if(last_modified_seconds == 0) { + if(!file_get_last_modified_time_seconds(filepath.data.c_str(), &last_modified_seconds)) + return true; + } + LocalAnimeSeason season; season.name = filepath.filename(); season.episodes = get_episodes_in_directory(filepath); - season.modified_time_seconds = modified_time_seconds; + season.modified_time_seconds = last_modified_seconds; if(season.episodes.empty()) return true; @@ -236,23 +245,33 @@ namespace QuickMedia { return anime_items; } - std::vector<LocalAnimeItem> get_anime_in_directory(const Path &directory) { + std::vector<LocalAnimeItem> get_anime_in_directory(const Path &directory, bool recursive) { std::vector<LocalAnimeItem> anime_items; - auto callback = [&anime_items](const Path &filepath, FileType file_type) -> bool { - time_t modified_time_seconds; - if(!file_get_last_modified_time_seconds(filepath.data.c_str(), &modified_time_seconds)) - return true; - + auto callback = [&anime_items, recursive](const Path &filepath, FileType file_type, time_t last_modified_seconds) -> bool { if(file_type == FileType::REGULAR) { - if(is_video_ext(filepath.ext())) - anime_items.push_back(LocalAnimeEpisode{ filepath, modified_time_seconds }); + if(is_video_ext(filepath.ext())) { + if(last_modified_seconds == 0) { + if(!file_get_last_modified_time_seconds(filepath.data.c_str(), &last_modified_seconds)) + return true; + } + + anime_items.push_back(LocalAnimeEpisode{ filepath.data, filepath, last_modified_seconds }); + } return true; } + + if(!recursive) + return true; + if(last_modified_seconds == 0) { + if(!file_get_last_modified_time_seconds(filepath.data.c_str(), &last_modified_seconds)) + return true; + } + LocalAnime anime; anime.name = filepath.filename(); anime.items = get_episodes_or_seasons_in_directory(filepath); - anime.modified_time_seconds = modified_time_seconds; + anime.modified_time_seconds = last_modified_seconds; if(anime.items.empty()) return true; @@ -352,7 +371,7 @@ namespace QuickMedia { PluginResult LocalAnimeSearchPage::lazy_fetch(BodyItems &result_items) { if(fetch_home_page) { fetch_home_page = false; - anime_items = get_anime_in_directory(get_config().local_anime.directory); + anime_items = get_anime_in_directory(get_config().local_anime.directory, get_config().local_anime.recursive); } std::unordered_map<std::string, WatchProgress> watch_progress = get_watch_progress_for_plugin("local-anime"); diff --git a/src/plugins/LocalManga.cpp b/src/plugins/LocalManga.cpp index fa0df27..21e8780 100644 --- a/src/plugins/LocalManga.cpp +++ b/src/plugins/LocalManga.cpp @@ -83,7 +83,7 @@ namespace QuickMedia { // Pages are sorted from 1.png to n.png static std::vector<LocalMangaPage> get_images_in_manga(const Path &directory) { std::vector<LocalMangaPage> page_list; - for_files_in_dir(directory, [&page_list](const Path &filepath, FileType file_type) -> bool { + for_files_in_dir(directory, [&page_list](const Path &filepath, FileType file_type, time_t) -> bool { if(file_type != FileType::REGULAR) return true; @@ -107,7 +107,7 @@ namespace QuickMedia { static std::vector<LocalMangaChapter> get_chapters_in_manga(std::string manga_name, const Path &directory, bool only_include_latest, bool include_pages, bool only_get_coverpage = false) { std::vector<LocalMangaChapter> chapter_list; - auto callback = [&chapter_list, &manga_name, only_include_latest, include_pages, only_get_coverpage](const Path &filepath, FileType file_type) -> bool { + auto callback = [&chapter_list, &manga_name, only_include_latest, include_pages, only_get_coverpage](const Path &filepath, FileType file_type, time_t last_modified_seconds) -> bool { if(file_type != FileType::DIRECTORY) return true; @@ -130,8 +130,11 @@ namespace QuickMedia { } } - if(!only_get_coverpage) - file_get_last_modified_time_seconds(filepath.data.c_str(), &local_manga_chapter.modified_time_seconds); + if(!only_get_coverpage) { + if(last_modified_seconds == 0) + file_get_last_modified_time_seconds(filepath.data.c_str(), &last_modified_seconds); + local_manga_chapter.modified_time_seconds = last_modified_seconds; + } chapter_list.push_back(std::move(local_manga_chapter)); return only_include_latest ? false : true; @@ -147,16 +150,20 @@ namespace QuickMedia { static std::vector<LocalManga> get_manga_in_directory(const Path &directory, bool only_get_coverpage) { std::vector<LocalManga> manga_list; - auto callback = [&manga_list, only_get_coverpage](const Path &filepath, FileType file_type) -> bool { + auto callback = [&manga_list, only_get_coverpage](const Path &filepath, FileType file_type, time_t last_modified_seconds) -> bool { if(file_type != FileType::DIRECTORY) return true; LocalManga local_manga; local_manga.name = filepath.filename(); local_manga.chapters = get_chapters_in_manga(local_manga.name, filepath, true, false, only_get_coverpage); - if(local_manga.chapters.empty() || !file_get_last_modified_time_seconds(filepath.data.c_str(), &local_manga.modified_time_seconds)) + if(local_manga.chapters.empty()) return true; + if(last_modified_seconds == 0) + file_get_last_modified_time_seconds(filepath.data.c_str(), &last_modified_seconds); + local_manga.modified_time_seconds = last_modified_seconds; + manga_list.push_back(std::move(local_manga)); return true; }; diff --git a/src/plugins/Matrix.cpp b/src/plugins/Matrix.cpp index c4aa076..c810d88 100644 --- a/src/plugins/Matrix.cpp +++ b/src/plugins/Matrix.cpp @@ -629,12 +629,15 @@ namespace QuickMedia { rooms_page->add_body_item(body_item); } - void MatrixQuickMedia::leave_room(RoomData *room, LeaveType leave_type, const std::string &reason) { + void MatrixQuickMedia::leave_room(RoomData *room, const std::string &event_id, LeaveType leave_type, const std::string &reason) { room_body_item_by_room.erase(room); rooms_page->remove_body_item_by_room_id(room->id); room_tags_page->remove_body_item_by_room_id(room->id); - if(leave_type != LeaveType::LEAVE) + if(leave_type != LeaveType::LEAVE && (event_id.empty() || !matrix->is_other_notification_read(event_id))) { show_notification("QuickMedia", reason); + if(!event_id.empty()) + matrix->mark_other_notification_as_read(event_id); + } } void MatrixQuickMedia::room_add_tag(RoomData *room, const std::string &tag) { @@ -1367,6 +1370,35 @@ namespace QuickMedia { } } + const char* MatrixVideoPage::get_title() const { + return ""; + } + + std::string MatrixVideoPage::get_filename() { + return filename; + } + + std::string MatrixVideoPage::get_download_url_for_clipboard(int max_height) { + std::string download_url = get_download_url(max_height); + size_t index = download_url.find("?access_token="); + if(index == std::string::npos) { + index = download_url.find("&access_token="); + if(index == std::string::npos) + return download_url; + } + + size_t end = download_url.find('&', index + 14); + if(end == std::string::npos) { + end = download_url.size(); + } else { + ++index; + ++end; + } + + download_url.erase(index, end - index); + return download_url; + } + MatrixChatPage::MatrixChatPage(Program *program, std::string room_id, MatrixRoomsPage *rooms_page, std::string jump_to_event_id) : Page(program), room_id(std::move(room_id)), rooms_page(rooms_page), jump_to_event_id(std::move(jump_to_event_id)) { @@ -1674,6 +1706,7 @@ namespace QuickMedia { load_silenced_invites(); load_custom_emoji_from_cache(); + load_other_notifications(); sync_thread = std::thread([this, matrix_cache_dir]() { FILE *sync_cache_file; @@ -1858,8 +1891,8 @@ namespace QuickMedia { fwrite(json_data.data(), 1, json_data.size(), sync_cache_file); file_overwrite(get_cache_dir().join("matrix").join(update_cache_file_name), "1"); // To make sure the cache format is up to date - malloc_trim(0); } + malloc_trim(0); fclose(sync_cache_file); } } @@ -2208,6 +2241,18 @@ namespace QuickMedia { parse_custom_emoji(json_root); } + void Matrix::load_other_notifications() { + std::string result; + if(file_get_content(get_storage_dir().join("matrix").join("other_notifications_read"), result) != 0) + return; + + other_notifications_read.clear(); + string_split_view(result, '\n', [this](const char *str, size_t line) { + other_notifications_read.insert(std::string(str, line)); + return true; + }); + } + PluginResult Matrix::parse_sync_account_data(const rapidjson::Value &account_data_json) { if(!account_data_json.IsObject()) return PluginResult::OK; @@ -2458,16 +2503,16 @@ namespace QuickMedia { return media_url.substr(start, end - start); } - static std::string get_avatar_thumbnail_url(const std::string &homeserver, const std::string &mxc_id) { + static std::string get_avatar_thumbnail_url(const std::string &homeserver, const std::string &access_token, const std::string &mxc_id) { if(mxc_id.empty()) return ""; std::string size = std::to_string(int(32 * get_config().scale)); - return homeserver + "/_matrix/media/r0/thumbnail/" + mxc_id + "?width=" + size + "&height=" + size + "&method=crop"; + return homeserver + "/_matrix/client/v1/media/thumbnail/" + mxc_id + "?width=" + size + "&height=" + size + "&method=crop&access_token=" + access_token; } std::string Matrix::get_media_url(const std::string &mxc_id) { - return homeserver + "/_matrix/media/r0/download/" + thumbnail_url_extract_media_id(mxc_id); + return homeserver + "/_matrix/client/v1/media/download/" + thumbnail_url_extract_media_id(mxc_id) + "?access_token=" + access_token; } RoomExtraData& Matrix::get_room_extra_data(RoomData *room) { @@ -2486,7 +2531,7 @@ namespace QuickMedia { std::string display_name = display_name_json.IsString() ? display_name_json.GetString() : user_id; std::string avatar_url = thumbnail_url_extract_media_id(avatar_url_str); if(!avatar_url.empty()) - avatar_url = get_avatar_thumbnail_url(homeserver, avatar_url); // TODO: Remove the constant strings around to reduce memory usage (6.3mb) + avatar_url = get_avatar_thumbnail_url(homeserver, access_token, avatar_url); // TODO: Remove the constant strings around to reduce memory usage (6.3mb) //auto user_info = std::make_shared<UserInfo>(room_data, user_id, std::move(display_name), std::move(avatar_url)); // Overwrites user data //room_data->add_user(user_info); @@ -2583,7 +2628,7 @@ namespace QuickMedia { return found_resolution; } - static std::string message_content_extract_thumbnail_url(const rapidjson::Value &content_json, const std::string &homeserver) { + static std::string message_content_extract_thumbnail_url(const rapidjson::Value &content_json, const std::string &homeserver, const std::string &access_token) { const rapidjson::Value &info_json = GetMember(content_json, "info"); if(info_json.IsObject()) { const rapidjson::Value &thumbnail_url_json = GetMember(info_json, "thumbnail_url"); @@ -2593,7 +2638,7 @@ namespace QuickMedia { return ""; thumbnail_str.erase(thumbnail_str.begin(), thumbnail_str.begin() + 6); - return homeserver + "/_matrix/media/r0/download/" + std::move(thumbnail_str); + return homeserver + "/_matrix/client/v1/media/download/" + std::move(thumbnail_str) + "?access_token=" + access_token; } } @@ -3180,7 +3225,7 @@ namespace QuickMedia { body = user_display_name + " changed his profile picture"; std::string new_avatar_url_str = thumbnail_url_extract_media_id(new_avatar_url_json.GetString()); if(!new_avatar_url_str.empty()) - new_avatar_url_str = get_avatar_thumbnail_url(homeserver, new_avatar_url_str); // TODO: Remove the constant strings around to reduce memory usage (6.3mb) + new_avatar_url_str = get_avatar_thumbnail_url(homeserver, access_token, new_avatar_url_str); // TODO: Remove the constant strings around to reduce memory usage (6.3mb) new_avatar_url = new_avatar_url_str; update_user_display_info = room_data->set_user_avatar_url(user, std::move(new_avatar_url_str), timestamp); } else if((!new_avatar_url_json.IsString() || new_avatar_url_json.GetStringLength() == 0) && prev_avatar_url_json.IsString()) { @@ -3406,8 +3451,8 @@ namespace QuickMedia { if(!url_json.IsString() || strncmp(url_json.GetString(), "mxc://", 6) != 0) return nullptr; - message->url = homeserver + "/_matrix/media/r0/download/" + (url_json.GetString() + 6); - message->thumbnail_url = message_content_extract_thumbnail_url(*content_json, homeserver); + message->url = homeserver + "/_matrix/client/v1/media/download/" + (url_json.GetString() + 6) + "?access_token=" + access_token; + message->thumbnail_url = message_content_extract_thumbnail_url(*content_json, homeserver, access_token); message_content_extract_thumbnail_size(*content_json, message->thumbnail_size); message->type = MessageType::IMAGE; } else if(!content_type.IsString() || strcmp(content_type.GetString(), "m.text") == 0) { @@ -3417,8 +3462,8 @@ namespace QuickMedia { if(!url_json.IsString() || strncmp(url_json.GetString(), "mxc://", 6) != 0) return nullptr; - message->url = homeserver + "/_matrix/media/r0/download/" + (url_json.GetString() + 6); - message->thumbnail_url = message_content_extract_thumbnail_url(*content_json, homeserver); + message->url = homeserver + "/_matrix/client/v1/media/download/" + (url_json.GetString() + 6) + "?access_token=" + access_token; + message->thumbnail_url = message_content_extract_thumbnail_url(*content_json, homeserver, access_token); message_content_extract_thumbnail_size(*content_json, message->thumbnail_size); message->type = MessageType::VIDEO; if(message->thumbnail_url.empty()) @@ -3428,7 +3473,7 @@ namespace QuickMedia { if(!url_json.IsString() || strncmp(url_json.GetString(), "mxc://", 6) != 0) return nullptr; - message->url = homeserver + "/_matrix/media/r0/download/" + (url_json.GetString() + 6); + message->url = homeserver + "/_matrix/client/v1/media/download/" + (url_json.GetString() + 6) + "?access_token=" + access_token; message->type = MessageType::AUDIO; prefix = "🎵 Play "; } else if(strcmp(content_type.GetString(), "m.file") == 0) { @@ -3436,7 +3481,7 @@ namespace QuickMedia { if(!url_json.IsString() || strncmp(url_json.GetString(), "mxc://", 6) != 0) return nullptr; - message->url = homeserver + "/_matrix/media/r0/download/" + (url_json.GetString() + 6); + message->url = homeserver + "/_matrix/client/v1/media/download/" + (url_json.GetString() + 6) + "?access_token=" + access_token; message->type = MessageType::FILE; prefix = "💾 Download "; } else if(strcmp(content_type.GetString(), "m.emote") == 0) { // this is a /me message, TODO: show /me messages differently @@ -3451,7 +3496,7 @@ namespace QuickMedia { prefix = geo_uri_json.GetString() + std::string(" | "); message->type = MessageType::TEXT; - message->thumbnail_url = message_content_extract_thumbnail_url(*content_json, homeserver); + message->thumbnail_url = message_content_extract_thumbnail_url(*content_json, homeserver, access_token); message_content_extract_thumbnail_size(*content_json, message->thumbnail_size); } else if(strcmp(content_type.GetString(), "m.server_notice") == 0) { // TODO: show server notices differently message->type = MessageType::TEXT; @@ -3611,7 +3656,7 @@ namespace QuickMedia { if(!url_json.IsString() || strncmp(url_json.GetString(), "mxc://", 6) != 0) continue; - update_room_avatar_url |= room_data->set_avatar_url(get_avatar_thumbnail_url(homeserver, thumbnail_url_extract_media_id(url_json.GetString())), item_timestamp); + update_room_avatar_url |= room_data->set_avatar_url(get_avatar_thumbnail_url(homeserver, access_token, thumbnail_url_extract_media_id(url_json.GetString())), item_timestamp); room_data->avatar_is_fallback = false; } else if(strcmp(type_json.GetString(), "m.room.topic") == 0) { const rapidjson::Value &content_json = GetMember(event_item_json, "content"); @@ -3884,6 +3929,10 @@ namespace QuickMedia { if(!type_json.IsString() || strcmp(type_json.GetString(), "m.room.member") != 0) continue; + const rapidjson::Value &event_id_json = GetMember(event_json, "event_id"); + if(!event_id_json.IsString()) + continue; + const rapidjson::Value &sender_json = GetMember(event_json, "sender"); if(!sender_json.IsString()) continue; @@ -3924,7 +3973,10 @@ namespace QuickMedia { if(!reason_str.empty()) desc += ", reason: " + reason_str; - ui_thread_tasks.push([this, room, leave_type, desc{std::move(desc)}]{ delegate->leave_room(room, leave_type, desc); }); + std::string event_id_str = event_id_json.GetString(); + ui_thread_tasks.push([this, room, event_id_str{std::move(event_id_str)}, leave_type, desc{std::move(desc)}]{ + delegate->leave_room(room, event_id_str, leave_type, desc); + }); remove_room(room_id_str); break; } @@ -4213,6 +4265,15 @@ namespace QuickMedia { return delegate; } + void Matrix::mark_other_notification_as_read(const std::string &event_id) { + other_notifications_read.insert(event_id); + file_append(get_storage_dir().join("matrix").join("other_notifications_read"), event_id + "\n"); + } + + bool Matrix::is_other_notification_read(const std::string &event_id) const { + return other_notifications_read.find(event_id) != other_notifications_read.end(); + } + PluginResult Matrix::post_message(RoomData *room, const std::string &body, std::string &event_id_response, const std::optional<UploadInfo> &file_info, const std::optional<UploadInfo> &thumbnail_info, const std::string &msgtype, const std::string &custom_transaction_id) { std::string transaction_id = custom_transaction_id; if(transaction_id.empty()) @@ -5040,7 +5101,10 @@ namespace QuickMedia { remote_homeserver_url = get_remote_homeserver_url(); char url[512]; - snprintf(url, sizeof(url), "%s/_matrix/media/r0/upload?filename=%s", remote_homeserver_url.c_str(), filename_escaped.c_str()); + snprintf(url, sizeof(url), "%s%s_matrix/media/r0/upload?filename=%s", + remote_homeserver_url.c_str(), + !remote_homeserver_url.empty() && remote_homeserver_url.back() == '/' ? "" : "/", + filename_escaped.c_str()); rapidjson::Document json_root; DownloadResult download_result = download_json(json_root, url, std::move(additional_args), true, &err_msg); @@ -5102,7 +5166,7 @@ namespace QuickMedia { //Path filter_cache_path = get_storage_dir().join("matrix").join("filter"); //remove(filter_cache_path.data.c_str()); - for_files_in_dir(get_cache_dir().join("matrix").join("events"), [](const Path &filepath, FileType) { + for_files_in_dir(get_cache_dir().join("matrix").join("events"), [](const Path &filepath, FileType, time_t) { remove(filepath.data.c_str()); return true; }); @@ -5629,7 +5693,7 @@ namespace QuickMedia { if(download_result == DownloadResult::OK) { RoomData *room = get_room_by_id(room_id); if(room) { - ui_thread_tasks.push([this, room]{ delegate->leave_room(room, LeaveType::LEAVE, ""); }); + ui_thread_tasks.push([this, room]{ delegate->leave_room(room, "", LeaveType::LEAVE, ""); }); remove_room(room_id); } } @@ -5746,7 +5810,7 @@ namespace QuickMedia { if(avatar_url_json.IsString()) { std::string avatar_url = thumbnail_url_extract_media_id(avatar_url_json.GetString()); if(!avatar_url.empty()) - avatar_url = get_avatar_thumbnail_url(homeserver, avatar_url); + avatar_url = get_avatar_thumbnail_url(homeserver, access_token, avatar_url); if(!avatar_url.empty()) room_body_item->thumbnail_url = std::move(avatar_url); @@ -5818,7 +5882,7 @@ namespace QuickMedia { if(avatar_url_json.IsString()) { std::string avatar_url = thumbnail_url_extract_media_id(std::string(avatar_url_json.GetString(), avatar_url_json.GetStringLength())); if(!avatar_url.empty()) - avatar_url = get_avatar_thumbnail_url(homeserver, avatar_url); + avatar_url = get_avatar_thumbnail_url(homeserver, access_token, avatar_url); body_item->thumbnail_url = std::move(avatar_url); } body_item->thumbnail_mask_type = ThumbnailMaskType::CIRCLE; @@ -6057,7 +6121,7 @@ namespace QuickMedia { if(avatar_url_json.IsString()) avatar_url = std::string(avatar_url_json.GetString(), avatar_url_json.GetStringLength()); if(!avatar_url.empty()) - avatar_url = get_avatar_thumbnail_url(homeserver, thumbnail_url_extract_media_id(avatar_url)); // TODO: Remove the constant strings around to reduce memory usage (6.3mb) + avatar_url = get_avatar_thumbnail_url(homeserver, access_token, thumbnail_url_extract_media_id(avatar_url)); // TODO: Remove the constant strings around to reduce memory usage (6.3mb) room->set_user_avatar_url(user, avatar_url, 0); room->set_user_display_name(user, display_name, 0); diff --git a/src/plugins/Youtube.cpp b/src/plugins/Youtube.cpp index 2c20f3f..7095e6b 100644 --- a/src/plugins/Youtube.cpp +++ b/src/plugins/Youtube.cpp @@ -21,7 +21,7 @@ extern "C" { #include <unistd.h> namespace QuickMedia { - static const char *youtube_client_version = "x-youtube-client-version: 2.20210622.10.00"; + static const char *youtube_client_version = "x-youtube-client-version: 2.20250327.01.00"; static const std::array<std::string, 11> invidious_urls = { "yewtu.be", "invidious.snopyta.org", @@ -3189,26 +3189,33 @@ namespace QuickMedia { } } - const Json::Value &playability_status_json = json_root["playabilityStatus"]; - if(playability_status_json.isObject()) { - const Json::Value &status_json = playability_status_json["status"]; - if(status_json.isString() && (strcmp(status_json.asCString(), "UNPLAYABLE") == 0 || strcmp(status_json.asCString(), "LOGIN_REQUIRED") == 0)) { - fprintf(stderr, "Failed to load youtube video, trying with yt-dlp instead\n"); - if(program->youtube_dl_extract_url(url, youtube_dl_video_fallback_url, youtube_dl_audio_fallback_url)) { - if(get_config().youtube.sponsorblock.enable) - sponsorblock_add_chapters(this, url, get_config().youtube.sponsorblock.min_votes, sponsor_segments); - use_youtube_dl_fallback = true; - return PluginResult::OK; - } else { - const Json::Value &reason_json = playability_status_json["reason"]; - if(reason_json.isString()) - err_str = reason_json.asString(); - fprintf(stderr, "Unable to play video, status: %s, reason: %s\n", status_json.asCString(), reason_json.isString() ? reason_json.asCString() : "Unknown"); - return PluginResult::ERR; - } - } + if(program->youtube_dl_extract_url(url, youtube_dl_video_fallback_url, youtube_dl_audio_fallback_url)) { + if(get_config().youtube.sponsorblock.enable) + sponsorblock_add_chapters(this, url, get_config().youtube.sponsorblock.min_votes, sponsor_segments); + use_youtube_dl_fallback = true; + return PluginResult::OK; } + // const Json::Value &playability_status_json = json_root["playabilityStatus"]; + // if(playability_status_json.isObject()) { + // const Json::Value &status_json = playability_status_json["status"]; + // if(status_json.isString() && (strcmp(status_json.asCString(), "UNPLAYABLE") == 0 || strcmp(status_json.asCString(), "LOGIN_REQUIRED") == 0)) { + // fprintf(stderr, "Failed to load youtube video, trying with yt-dlp instead\n"); + // if(program->youtube_dl_extract_url(url, youtube_dl_video_fallback_url, youtube_dl_audio_fallback_url)) { + // if(get_config().youtube.sponsorblock.enable) + // sponsorblock_add_chapters(this, url, get_config().youtube.sponsorblock.min_votes, sponsor_segments); + // use_youtube_dl_fallback = true; + // return PluginResult::OK; + // } else { + // const Json::Value &reason_json = playability_status_json["reason"]; + // if(reason_json.isString()) + // err_str = reason_json.asString(); + // fprintf(stderr, "Unable to play video, status: %s, reason: %s\n", status_json.asCString(), reason_json.isString() ? reason_json.asCString() : "Unknown"); + // return PluginResult::ERR; + // } + // } + // } + const Json::Value *streaming_data_json = &json_root["streamingData"]; if(!streaming_data_json->isObject()) return PluginResult::ERR; @@ -3254,7 +3261,51 @@ namespace QuickMedia { const int num_request_types = 1; std::string request_data[num_request_types] = { R"END( - {"context":{"client":{"hl":"en","gl":"US","clientName":"IOS","clientVersion":"17.33.2","deviceModel":"iPhone14,3"}},"videoId":"%VIDEO_ID%"} +{ + "context": { + "client": { + "hl": "en", + "gl": "SE", + "deviceMake": "", + "deviceModel": "", + "userAgent": "Mozilla/5.0 (X11; Linux x86_64; rv:136.0) Gecko/20100101 Firefox/136.0,gzip(gfe)", + "clientName": "WEB", + "clientVersion": "2.20250327.01.00", + "osName": "X11", + "osVersion": "", + "originalUrl": "https://www.youtube.com/watch?v=%VIDEO_ID%", + "platform": "DESKTOP", + "clientFormFactor": "UNKNOWN_FORM_FACTOR", + "userInterfaceTheme": "USER_INTERFACE_THEME_DARK", + "timeZone": "Europe/Paris", + "browserName": "Firefox", + "browserVersion": "136.0", + "acceptHeader": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", + "clientScreen": "WATCH", + "mainAppWebInfo": { + "graftUrl": "/watch?v=%VIDEO_ID%", + "pwaInstallabilityStatus": "PWA_INSTALLABILITY_STATUS_UNKNOWN", + "webDisplayMode": "WEB_DISPLAY_MODE_BROWSER", + "isWebNativeShareAvailable": false + } + } + }, + "videoId": "%VIDEO_ID%", + "playbackContext": { + "contentPlaybackContext": { + "currentUrl": "/watch?v=%VIDEO_ID%", + "vis": 0, + "splay": false, + "autoCaptionsDefaultOn": false, + "autonavState": "STATE_OFF", + "html5Preference": "HTML5_PREF_WANTS", + "referer": "https://www.youtube.com/", + "lactMilliseconds": "-1" + } + }, + "racyCheckOk": false, + "contentCheckOk": false +} )END", }; @@ -3263,7 +3314,7 @@ R"END( }; std::string client_versions[num_request_types] = { - "2.20210622.10.00" + "2.20250327.01.00" }; for(int i = 0; i < num_request_types; ++i) { @@ -3280,7 +3331,7 @@ R"END( additional_args.insert(additional_args.end(), cookies.begin(), cookies.end()); Json::Value json_root; - DownloadResult download_result = download_json(json_root, "https://www.youtube.com/youtubei/v1/player?key=" + api_key + "&gl=US&hl=en&prettyPrint=false", additional_args, true); + DownloadResult download_result = download_json(json_root, "https://www.youtube.com/youtubei/v1/player?prettyPrint=false", additional_args, true); if(download_result != DownloadResult::OK) continue; @@ -3423,6 +3474,7 @@ R"END( continue; } + //video_format.base.url += "&alr=yes"; video_formats.push_back(std::move(video_format)); } else if(strncmp(youtube_format_base.mime_type.c_str(), "audio/", 6) == 0) { // Some youtube videos have multiple audio tracks and sometimes the audio tracks are in the same language @@ -3445,6 +3497,7 @@ R"END( continue; } + //audio_format.base.url += "&alr=yes"; audio_formats.push_back(std::move(audio_format)); } } diff --git a/src/plugins/utils/EpisodeNameParser.cpp b/src/plugins/utils/EpisodeNameParser.cpp index 8610ab0..8141421 100644 --- a/src/plugins/utils/EpisodeNameParser.cpp +++ b/src/plugins/utils/EpisodeNameParser.cpp @@ -108,6 +108,12 @@ namespace QuickMedia { return (c >= '0' && c <= '9') || c == '.'; } + static void remove_trailing_characters(std::string_view &str, char c) { + while(!str.empty() && str.back() == c) { + str.remove_suffix(1); + } + } + static std::string_view episode_name_extract_episode(std::string_view &episode_name) { episode_name = strip_left(episode_name); size_t i = 0; @@ -120,6 +126,7 @@ namespace QuickMedia { return {}; std::string_view episode = episode_name.substr(0, i); + remove_trailing_characters(episode, '.'); episode_name.remove_prefix(i + 1); return episode; } diff --git a/src/plugins/utils/aes.c b/src/plugins/utils/aes.c new file mode 100644 index 0000000..2bd115e --- /dev/null +++ b/src/plugins/utils/aes.c @@ -0,0 +1,571 @@ +/* + +This is an implementation of the AES algorithm, specifically ECB, CTR and CBC mode. +Block size can be chosen in aes.h - available choices are AES128, AES192, AES256. + +The implementation is verified against the test vectors in: + National Institute of Standards and Technology Special Publication 800-38A 2001 ED + +ECB-AES128 +---------- + + plain-text: + 6bc1bee22e409f96e93d7e117393172a + ae2d8a571e03ac9c9eb76fac45af8e51 + 30c81c46a35ce411e5fbc1191a0a52ef + f69f2445df4f9b17ad2b417be66c3710 + + key: + 2b7e151628aed2a6abf7158809cf4f3c + + resulting cipher + 3ad77bb40d7a3660a89ecaf32466ef97 + f5d3d58503b9699de785895a96fdbaaf + 43b1cd7f598ece23881b00e3ed030688 + 7b0c785e27e8ad3f8223207104725dd4 + + +NOTE: String length must be evenly divisible by 16byte (str_len % 16 == 0) + You should pad the end of the string with zeros if this is not the case. + For AES192/256 the key size is proportionally larger. + +*/ + + +/*****************************************************************************/ +/* Includes: */ +/*****************************************************************************/ +#include <string.h> // CBC mode, for memset +#include "aes.h" + +/*****************************************************************************/ +/* Defines: */ +/*****************************************************************************/ +// The number of columns comprising a state in AES. This is a constant in AES. Value=4 +#define Nb 4 + +#if defined(AES256) && (AES256 == 1) + #define Nk 8 + #define Nr 14 +#elif defined(AES192) && (AES192 == 1) + #define Nk 6 + #define Nr 12 +#else + #define Nk 4 // The number of 32 bit words in a key. + #define Nr 10 // The number of rounds in AES Cipher. +#endif + +// jcallan@github points out that declaring Multiply as a function +// reduces code size considerably with the Keil ARM compiler. +// See this link for more information: https://github.com/kokke/tiny-AES-C/pull/3 +#ifndef MULTIPLY_AS_A_FUNCTION + #define MULTIPLY_AS_A_FUNCTION 0 +#endif + + + + +/*****************************************************************************/ +/* Private variables: */ +/*****************************************************************************/ +// state - array holding the intermediate results during decryption. +typedef uint8_t state_t[4][4]; + + + +// The lookup-tables are marked const so they can be placed in read-only storage instead of RAM +// The numbers below can be computed dynamically trading ROM for RAM - +// This can be useful in (embedded) bootloader applications, where ROM is often limited. +static const uint8_t sbox[256] = { + //0 1 2 3 4 5 6 7 8 9 A B C D E F + 0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, 0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76, + 0xca, 0x82, 0xc9, 0x7d, 0xfa, 0x59, 0x47, 0xf0, 0xad, 0xd4, 0xa2, 0xaf, 0x9c, 0xa4, 0x72, 0xc0, + 0xb7, 0xfd, 0x93, 0x26, 0x36, 0x3f, 0xf7, 0xcc, 0x34, 0xa5, 0xe5, 0xf1, 0x71, 0xd8, 0x31, 0x15, + 0x04, 0xc7, 0x23, 0xc3, 0x18, 0x96, 0x05, 0x9a, 0x07, 0x12, 0x80, 0xe2, 0xeb, 0x27, 0xb2, 0x75, + 0x09, 0x83, 0x2c, 0x1a, 0x1b, 0x6e, 0x5a, 0xa0, 0x52, 0x3b, 0xd6, 0xb3, 0x29, 0xe3, 0x2f, 0x84, + 0x53, 0xd1, 0x00, 0xed, 0x20, 0xfc, 0xb1, 0x5b, 0x6a, 0xcb, 0xbe, 0x39, 0x4a, 0x4c, 0x58, 0xcf, + 0xd0, 0xef, 0xaa, 0xfb, 0x43, 0x4d, 0x33, 0x85, 0x45, 0xf9, 0x02, 0x7f, 0x50, 0x3c, 0x9f, 0xa8, + 0x51, 0xa3, 0x40, 0x8f, 0x92, 0x9d, 0x38, 0xf5, 0xbc, 0xb6, 0xda, 0x21, 0x10, 0xff, 0xf3, 0xd2, + 0xcd, 0x0c, 0x13, 0xec, 0x5f, 0x97, 0x44, 0x17, 0xc4, 0xa7, 0x7e, 0x3d, 0x64, 0x5d, 0x19, 0x73, + 0x60, 0x81, 0x4f, 0xdc, 0x22, 0x2a, 0x90, 0x88, 0x46, 0xee, 0xb8, 0x14, 0xde, 0x5e, 0x0b, 0xdb, + 0xe0, 0x32, 0x3a, 0x0a, 0x49, 0x06, 0x24, 0x5c, 0xc2, 0xd3, 0xac, 0x62, 0x91, 0x95, 0xe4, 0x79, + 0xe7, 0xc8, 0x37, 0x6d, 0x8d, 0xd5, 0x4e, 0xa9, 0x6c, 0x56, 0xf4, 0xea, 0x65, 0x7a, 0xae, 0x08, + 0xba, 0x78, 0x25, 0x2e, 0x1c, 0xa6, 0xb4, 0xc6, 0xe8, 0xdd, 0x74, 0x1f, 0x4b, 0xbd, 0x8b, 0x8a, + 0x70, 0x3e, 0xb5, 0x66, 0x48, 0x03, 0xf6, 0x0e, 0x61, 0x35, 0x57, 0xb9, 0x86, 0xc1, 0x1d, 0x9e, + 0xe1, 0xf8, 0x98, 0x11, 0x69, 0xd9, 0x8e, 0x94, 0x9b, 0x1e, 0x87, 0xe9, 0xce, 0x55, 0x28, 0xdf, + 0x8c, 0xa1, 0x89, 0x0d, 0xbf, 0xe6, 0x42, 0x68, 0x41, 0x99, 0x2d, 0x0f, 0xb0, 0x54, 0xbb, 0x16 }; + +#if (defined(CBC) && CBC == 1) || (defined(ECB) && ECB == 1) +static const uint8_t rsbox[256] = { + 0x52, 0x09, 0x6a, 0xd5, 0x30, 0x36, 0xa5, 0x38, 0xbf, 0x40, 0xa3, 0x9e, 0x81, 0xf3, 0xd7, 0xfb, + 0x7c, 0xe3, 0x39, 0x82, 0x9b, 0x2f, 0xff, 0x87, 0x34, 0x8e, 0x43, 0x44, 0xc4, 0xde, 0xe9, 0xcb, + 0x54, 0x7b, 0x94, 0x32, 0xa6, 0xc2, 0x23, 0x3d, 0xee, 0x4c, 0x95, 0x0b, 0x42, 0xfa, 0xc3, 0x4e, + 0x08, 0x2e, 0xa1, 0x66, 0x28, 0xd9, 0x24, 0xb2, 0x76, 0x5b, 0xa2, 0x49, 0x6d, 0x8b, 0xd1, 0x25, + 0x72, 0xf8, 0xf6, 0x64, 0x86, 0x68, 0x98, 0x16, 0xd4, 0xa4, 0x5c, 0xcc, 0x5d, 0x65, 0xb6, 0x92, + 0x6c, 0x70, 0x48, 0x50, 0xfd, 0xed, 0xb9, 0xda, 0x5e, 0x15, 0x46, 0x57, 0xa7, 0x8d, 0x9d, 0x84, + 0x90, 0xd8, 0xab, 0x00, 0x8c, 0xbc, 0xd3, 0x0a, 0xf7, 0xe4, 0x58, 0x05, 0xb8, 0xb3, 0x45, 0x06, + 0xd0, 0x2c, 0x1e, 0x8f, 0xca, 0x3f, 0x0f, 0x02, 0xc1, 0xaf, 0xbd, 0x03, 0x01, 0x13, 0x8a, 0x6b, + 0x3a, 0x91, 0x11, 0x41, 0x4f, 0x67, 0xdc, 0xea, 0x97, 0xf2, 0xcf, 0xce, 0xf0, 0xb4, 0xe6, 0x73, + 0x96, 0xac, 0x74, 0x22, 0xe7, 0xad, 0x35, 0x85, 0xe2, 0xf9, 0x37, 0xe8, 0x1c, 0x75, 0xdf, 0x6e, + 0x47, 0xf1, 0x1a, 0x71, 0x1d, 0x29, 0xc5, 0x89, 0x6f, 0xb7, 0x62, 0x0e, 0xaa, 0x18, 0xbe, 0x1b, + 0xfc, 0x56, 0x3e, 0x4b, 0xc6, 0xd2, 0x79, 0x20, 0x9a, 0xdb, 0xc0, 0xfe, 0x78, 0xcd, 0x5a, 0xf4, + 0x1f, 0xdd, 0xa8, 0x33, 0x88, 0x07, 0xc7, 0x31, 0xb1, 0x12, 0x10, 0x59, 0x27, 0x80, 0xec, 0x5f, + 0x60, 0x51, 0x7f, 0xa9, 0x19, 0xb5, 0x4a, 0x0d, 0x2d, 0xe5, 0x7a, 0x9f, 0x93, 0xc9, 0x9c, 0xef, + 0xa0, 0xe0, 0x3b, 0x4d, 0xae, 0x2a, 0xf5, 0xb0, 0xc8, 0xeb, 0xbb, 0x3c, 0x83, 0x53, 0x99, 0x61, + 0x17, 0x2b, 0x04, 0x7e, 0xba, 0x77, 0xd6, 0x26, 0xe1, 0x69, 0x14, 0x63, 0x55, 0x21, 0x0c, 0x7d }; +#endif + +// The round constant word array, Rcon[i], contains the values given by +// x to the power (i-1) being powers of x (x is denoted as {02}) in the field GF(2^8) +static const uint8_t Rcon[11] = { + 0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36 }; + +/* + * Jordan Goulder points out in PR #12 (https://github.com/kokke/tiny-AES-C/pull/12), + * that you can remove most of the elements in the Rcon array, because they are unused. + * + * From Wikipedia's article on the Rijndael key schedule @ https://en.wikipedia.org/wiki/Rijndael_key_schedule#Rcon + * + * "Only the first some of these constants are actually used – up to rcon[10] for AES-128 (as 11 round keys are needed), + * up to rcon[8] for AES-192, up to rcon[7] for AES-256. rcon[0] is not used in AES algorithm." + */ + + +/*****************************************************************************/ +/* Private functions: */ +/*****************************************************************************/ +/* +static uint8_t getSBoxValue(uint8_t num) +{ + return sbox[num]; +} +*/ +#define getSBoxValue(num) (sbox[(num)]) + +// This function produces Nb(Nr+1) round keys. The round keys are used in each round to decrypt the states. +static void KeyExpansion(uint8_t* RoundKey, const uint8_t* Key) +{ + unsigned i, j, k; + uint8_t tempa[4]; // Used for the column/row operations + + // The first round key is the key itself. + for (i = 0; i < Nk; ++i) + { + RoundKey[(i * 4) + 0] = Key[(i * 4) + 0]; + RoundKey[(i * 4) + 1] = Key[(i * 4) + 1]; + RoundKey[(i * 4) + 2] = Key[(i * 4) + 2]; + RoundKey[(i * 4) + 3] = Key[(i * 4) + 3]; + } + + // All other round keys are found from the previous round keys. + for (i = Nk; i < Nb * (Nr + 1); ++i) + { + { + k = (i - 1) * 4; + tempa[0]=RoundKey[k + 0]; + tempa[1]=RoundKey[k + 1]; + tempa[2]=RoundKey[k + 2]; + tempa[3]=RoundKey[k + 3]; + + } + + if (i % Nk == 0) + { + // This function shifts the 4 bytes in a word to the left once. + // [a0,a1,a2,a3] becomes [a1,a2,a3,a0] + + // Function RotWord() + { + const uint8_t u8tmp = tempa[0]; + tempa[0] = tempa[1]; + tempa[1] = tempa[2]; + tempa[2] = tempa[3]; + tempa[3] = u8tmp; + } + + // SubWord() is a function that takes a four-byte input word and + // applies the S-box to each of the four bytes to produce an output word. + + // Function Subword() + { + tempa[0] = getSBoxValue(tempa[0]); + tempa[1] = getSBoxValue(tempa[1]); + tempa[2] = getSBoxValue(tempa[2]); + tempa[3] = getSBoxValue(tempa[3]); + } + + tempa[0] = tempa[0] ^ Rcon[i/Nk]; + } +#if defined(AES256) && (AES256 == 1) + if (i % Nk == 4) + { + // Function Subword() + { + tempa[0] = getSBoxValue(tempa[0]); + tempa[1] = getSBoxValue(tempa[1]); + tempa[2] = getSBoxValue(tempa[2]); + tempa[3] = getSBoxValue(tempa[3]); + } + } +#endif + j = i * 4; k=(i - Nk) * 4; + RoundKey[j + 0] = RoundKey[k + 0] ^ tempa[0]; + RoundKey[j + 1] = RoundKey[k + 1] ^ tempa[1]; + RoundKey[j + 2] = RoundKey[k + 2] ^ tempa[2]; + RoundKey[j + 3] = RoundKey[k + 3] ^ tempa[3]; + } +} + +void AES_init_ctx(struct AES_ctx* ctx, const uint8_t* key) +{ + KeyExpansion(ctx->RoundKey, key); +} +#if (defined(CBC) && (CBC == 1)) || (defined(CTR) && (CTR == 1)) +void AES_init_ctx_iv(struct AES_ctx* ctx, const uint8_t* key, const uint8_t* iv) +{ + KeyExpansion(ctx->RoundKey, key); + memcpy (ctx->Iv, iv, AES_BLOCKLEN); +} +void AES_ctx_set_iv(struct AES_ctx* ctx, const uint8_t* iv) +{ + memcpy (ctx->Iv, iv, AES_BLOCKLEN); +} +#endif + +// This function adds the round key to state. +// The round key is added to the state by an XOR function. +static void AddRoundKey(uint8_t round, state_t* state, const uint8_t* RoundKey) +{ + uint8_t i,j; + for (i = 0; i < 4; ++i) + { + for (j = 0; j < 4; ++j) + { + (*state)[i][j] ^= RoundKey[(round * Nb * 4) + (i * Nb) + j]; + } + } +} + +// The SubBytes Function Substitutes the values in the +// state matrix with values in an S-box. +static void SubBytes(state_t* state) +{ + uint8_t i, j; + for (i = 0; i < 4; ++i) + { + for (j = 0; j < 4; ++j) + { + (*state)[j][i] = getSBoxValue((*state)[j][i]); + } + } +} + +// The ShiftRows() function shifts the rows in the state to the left. +// Each row is shifted with different offset. +// Offset = Row number. So the first row is not shifted. +static void ShiftRows(state_t* state) +{ + uint8_t temp; + + // Rotate first row 1 columns to left + temp = (*state)[0][1]; + (*state)[0][1] = (*state)[1][1]; + (*state)[1][1] = (*state)[2][1]; + (*state)[2][1] = (*state)[3][1]; + (*state)[3][1] = temp; + + // Rotate second row 2 columns to left + temp = (*state)[0][2]; + (*state)[0][2] = (*state)[2][2]; + (*state)[2][2] = temp; + + temp = (*state)[1][2]; + (*state)[1][2] = (*state)[3][2]; + (*state)[3][2] = temp; + + // Rotate third row 3 columns to left + temp = (*state)[0][3]; + (*state)[0][3] = (*state)[3][3]; + (*state)[3][3] = (*state)[2][3]; + (*state)[2][3] = (*state)[1][3]; + (*state)[1][3] = temp; +} + +static uint8_t xtime(uint8_t x) +{ + return ((x<<1) ^ (((x>>7) & 1) * 0x1b)); +} + +// MixColumns function mixes the columns of the state matrix +static void MixColumns(state_t* state) +{ + uint8_t i; + uint8_t Tmp, Tm, t; + for (i = 0; i < 4; ++i) + { + t = (*state)[i][0]; + Tmp = (*state)[i][0] ^ (*state)[i][1] ^ (*state)[i][2] ^ (*state)[i][3] ; + Tm = (*state)[i][0] ^ (*state)[i][1] ; Tm = xtime(Tm); (*state)[i][0] ^= Tm ^ Tmp ; + Tm = (*state)[i][1] ^ (*state)[i][2] ; Tm = xtime(Tm); (*state)[i][1] ^= Tm ^ Tmp ; + Tm = (*state)[i][2] ^ (*state)[i][3] ; Tm = xtime(Tm); (*state)[i][2] ^= Tm ^ Tmp ; + Tm = (*state)[i][3] ^ t ; Tm = xtime(Tm); (*state)[i][3] ^= Tm ^ Tmp ; + } +} + +// Multiply is used to multiply numbers in the field GF(2^8) +// Note: The last call to xtime() is unneeded, but often ends up generating a smaller binary +// The compiler seems to be able to vectorize the operation better this way. +// See https://github.com/kokke/tiny-AES-c/pull/34 +#if MULTIPLY_AS_A_FUNCTION +static uint8_t Multiply(uint8_t x, uint8_t y) +{ + return (((y & 1) * x) ^ + ((y>>1 & 1) * xtime(x)) ^ + ((y>>2 & 1) * xtime(xtime(x))) ^ + ((y>>3 & 1) * xtime(xtime(xtime(x)))) ^ + ((y>>4 & 1) * xtime(xtime(xtime(xtime(x)))))); /* this last call to xtime() can be omitted */ + } +#else +#define Multiply(x, y) \ + ( ((y & 1) * x) ^ \ + ((y>>1 & 1) * xtime(x)) ^ \ + ((y>>2 & 1) * xtime(xtime(x))) ^ \ + ((y>>3 & 1) * xtime(xtime(xtime(x)))) ^ \ + ((y>>4 & 1) * xtime(xtime(xtime(xtime(x)))))) \ + +#endif + +#if (defined(CBC) && CBC == 1) || (defined(ECB) && ECB == 1) +/* +static uint8_t getSBoxInvert(uint8_t num) +{ + return rsbox[num]; +} +*/ +#define getSBoxInvert(num) (rsbox[(num)]) + +// MixColumns function mixes the columns of the state matrix. +// The method used to multiply may be difficult to understand for the inexperienced. +// Please use the references to gain more information. +static void InvMixColumns(state_t* state) +{ + int i; + uint8_t a, b, c, d; + for (i = 0; i < 4; ++i) + { + a = (*state)[i][0]; + b = (*state)[i][1]; + c = (*state)[i][2]; + d = (*state)[i][3]; + + (*state)[i][0] = Multiply(a, 0x0e) ^ Multiply(b, 0x0b) ^ Multiply(c, 0x0d) ^ Multiply(d, 0x09); + (*state)[i][1] = Multiply(a, 0x09) ^ Multiply(b, 0x0e) ^ Multiply(c, 0x0b) ^ Multiply(d, 0x0d); + (*state)[i][2] = Multiply(a, 0x0d) ^ Multiply(b, 0x09) ^ Multiply(c, 0x0e) ^ Multiply(d, 0x0b); + (*state)[i][3] = Multiply(a, 0x0b) ^ Multiply(b, 0x0d) ^ Multiply(c, 0x09) ^ Multiply(d, 0x0e); + } +} + + +// The SubBytes Function Substitutes the values in the +// state matrix with values in an S-box. +static void InvSubBytes(state_t* state) +{ + uint8_t i, j; + for (i = 0; i < 4; ++i) + { + for (j = 0; j < 4; ++j) + { + (*state)[j][i] = getSBoxInvert((*state)[j][i]); + } + } +} + +static void InvShiftRows(state_t* state) +{ + uint8_t temp; + + // Rotate first row 1 columns to right + temp = (*state)[3][1]; + (*state)[3][1] = (*state)[2][1]; + (*state)[2][1] = (*state)[1][1]; + (*state)[1][1] = (*state)[0][1]; + (*state)[0][1] = temp; + + // Rotate second row 2 columns to right + temp = (*state)[0][2]; + (*state)[0][2] = (*state)[2][2]; + (*state)[2][2] = temp; + + temp = (*state)[1][2]; + (*state)[1][2] = (*state)[3][2]; + (*state)[3][2] = temp; + + // Rotate third row 3 columns to right + temp = (*state)[0][3]; + (*state)[0][3] = (*state)[1][3]; + (*state)[1][3] = (*state)[2][3]; + (*state)[2][3] = (*state)[3][3]; + (*state)[3][3] = temp; +} +#endif // #if (defined(CBC) && CBC == 1) || (defined(ECB) && ECB == 1) + +// Cipher is the main function that encrypts the PlainText. +static void Cipher(state_t* state, const uint8_t* RoundKey) +{ + uint8_t round = 0; + + // Add the First round key to the state before starting the rounds. + AddRoundKey(0, state, RoundKey); + + // There will be Nr rounds. + // The first Nr-1 rounds are identical. + // These Nr rounds are executed in the loop below. + // Last one without MixColumns() + for (round = 1; ; ++round) + { + SubBytes(state); + ShiftRows(state); + if (round == Nr) { + break; + } + MixColumns(state); + AddRoundKey(round, state, RoundKey); + } + // Add round key to last round + AddRoundKey(Nr, state, RoundKey); +} + +#if (defined(CBC) && CBC == 1) || (defined(ECB) && ECB == 1) +static void InvCipher(state_t* state, const uint8_t* RoundKey) +{ + uint8_t round = 0; + + // Add the First round key to the state before starting the rounds. + AddRoundKey(Nr, state, RoundKey); + + // There will be Nr rounds. + // The first Nr-1 rounds are identical. + // These Nr rounds are executed in the loop below. + // Last one without InvMixColumn() + for (round = (Nr - 1); ; --round) + { + InvShiftRows(state); + InvSubBytes(state); + AddRoundKey(round, state, RoundKey); + if (round == 0) { + break; + } + InvMixColumns(state); + } + +} +#endif // #if (defined(CBC) && CBC == 1) || (defined(ECB) && ECB == 1) + +/*****************************************************************************/ +/* Public functions: */ +/*****************************************************************************/ +#if defined(ECB) && (ECB == 1) + + +void AES_ECB_encrypt(const struct AES_ctx* ctx, uint8_t* buf) +{ + // The next function call encrypts the PlainText with the Key using AES algorithm. + Cipher((state_t*)buf, ctx->RoundKey); +} + +void AES_ECB_decrypt(const struct AES_ctx* ctx, uint8_t* buf) +{ + // The next function call decrypts the PlainText with the Key using AES algorithm. + InvCipher((state_t*)buf, ctx->RoundKey); +} + + +#endif // #if defined(ECB) && (ECB == 1) + + + + + +#if defined(CBC) && (CBC == 1) + + +static void XorWithIv(uint8_t* buf, const uint8_t* Iv) +{ + uint8_t i; + for (i = 0; i < AES_BLOCKLEN; ++i) // The block in AES is always 128bit no matter the key size + { + buf[i] ^= Iv[i]; + } +} + +void AES_CBC_encrypt_buffer(struct AES_ctx *ctx, uint8_t* buf, size_t length) +{ + size_t i; + uint8_t *Iv = ctx->Iv; + for (i = 0; i < length; i += AES_BLOCKLEN) + { + XorWithIv(buf, Iv); + Cipher((state_t*)buf, ctx->RoundKey); + Iv = buf; + buf += AES_BLOCKLEN; + } + /* store Iv in ctx for next call */ + memcpy(ctx->Iv, Iv, AES_BLOCKLEN); +} + +void AES_CBC_decrypt_buffer(struct AES_ctx* ctx, uint8_t* buf, size_t length) +{ + size_t i; + uint8_t storeNextIv[AES_BLOCKLEN]; + for (i = 0; i < length; i += AES_BLOCKLEN) + { + memcpy(storeNextIv, buf, AES_BLOCKLEN); + InvCipher((state_t*)buf, ctx->RoundKey); + XorWithIv(buf, ctx->Iv); + memcpy(ctx->Iv, storeNextIv, AES_BLOCKLEN); + buf += AES_BLOCKLEN; + } + +} + +#endif // #if defined(CBC) && (CBC == 1) + + + +#if defined(CTR) && (CTR == 1) + +/* Symmetrical operation: same function for encrypting as for decrypting. Note any IV/nonce should never be reused with the same key */ +void AES_CTR_xcrypt_buffer(struct AES_ctx* ctx, uint8_t* buf, size_t length) +{ + uint8_t buffer[AES_BLOCKLEN]; + + size_t i; + int bi; + for (i = 0, bi = AES_BLOCKLEN; i < length; ++i, ++bi) + { + if (bi == AES_BLOCKLEN) /* we need to regen xor compliment in buffer */ + { + + memcpy(buffer, ctx->Iv, AES_BLOCKLEN); + Cipher((state_t*)buffer,ctx->RoundKey); + + /* Increment Iv and handle overflow */ + for (bi = (AES_BLOCKLEN - 1); bi >= 0; --bi) + { + /* inc will overflow */ + if (ctx->Iv[bi] == 255) + { + ctx->Iv[bi] = 0; + continue; + } + ctx->Iv[bi] += 1; + break; + } + bi = 0; + } + + buf[i] = (buf[i] ^ buffer[bi]); + } +} + +#endif // #if defined(CTR) && (CTR == 1) diff --git a/src/plugins/utils/aes.h b/src/plugins/utils/aes.h new file mode 100644 index 0000000..dcf8521 --- /dev/null +++ b/src/plugins/utils/aes.h @@ -0,0 +1,91 @@ +#ifndef _AES_H_ +#define _AES_H_ + +#include <stdint.h> +#include <stddef.h> + +// #define the macros below to 1/0 to enable/disable the mode of operation. +// +// CBC enables AES encryption in CBC-mode of operation. +// CTR enables encryption in counter-mode. +// ECB enables the basic ECB 16-byte block algorithm. All can be enabled simultaneously. + +// The #ifndef-guard allows it to be configured before #include'ing or at compile time. +#ifndef CBC + #define CBC 1 +#endif + +// #ifndef ECB +// #define ECB 1 +// #endif + +// #ifndef CTR +// #define CTR 1 +// #endif + + +//#define AES128 1 +//#define AES192 1 +#define AES256 1 + +#define AES_BLOCKLEN 16 // Block length in bytes - AES is 128b block only + +#if defined(AES256) && (AES256 == 1) + #define AES_KEYLEN 32 + #define AES_keyExpSize 240 +#elif defined(AES192) && (AES192 == 1) + #define AES_KEYLEN 24 + #define AES_keyExpSize 208 +#else + #define AES_KEYLEN 16 // Key length in bytes + #define AES_keyExpSize 176 +#endif + +struct AES_ctx +{ + uint8_t RoundKey[AES_keyExpSize]; +#if (defined(CBC) && (CBC == 1)) || (defined(CTR) && (CTR == 1)) + uint8_t Iv[AES_BLOCKLEN]; +#endif +}; + +void AES_init_ctx(struct AES_ctx* ctx, const uint8_t* key); +#if (defined(CBC) && (CBC == 1)) || (defined(CTR) && (CTR == 1)) +void AES_init_ctx_iv(struct AES_ctx* ctx, const uint8_t* key, const uint8_t* iv); +void AES_ctx_set_iv(struct AES_ctx* ctx, const uint8_t* iv); +#endif + +#if defined(ECB) && (ECB == 1) +// buffer size is exactly AES_BLOCKLEN bytes; +// you need only AES_init_ctx as IV is not used in ECB +// NB: ECB is considered insecure for most uses +void AES_ECB_encrypt(const struct AES_ctx* ctx, uint8_t* buf); +void AES_ECB_decrypt(const struct AES_ctx* ctx, uint8_t* buf); + +#endif // #if defined(ECB) && (ECB == !) + + +#if defined(CBC) && (CBC == 1) +// buffer size MUST be mutile of AES_BLOCKLEN; +// Suggest https://en.wikipedia.org/wiki/Padding_(cryptography)#PKCS7 for padding scheme +// NOTES: you need to set IV in ctx via AES_init_ctx_iv() or AES_ctx_set_iv() +// no IV should ever be reused with the same key +void AES_CBC_encrypt_buffer(struct AES_ctx* ctx, uint8_t* buf, size_t length); +void AES_CBC_decrypt_buffer(struct AES_ctx* ctx, uint8_t* buf, size_t length); + +#endif // #if defined(CBC) && (CBC == 1) + + +#if defined(CTR) && (CTR == 1) + +// Same function for encrypting as for decrypting. +// IV is incremented for every block, and used after encryption as XOR-compliment for output +// Suggesting https://en.wikipedia.org/wiki/Padding_(cryptography)#PKCS7 for padding scheme +// NOTES: you need to set IV in ctx with AES_init_ctx_iv() or AES_ctx_set_iv() +// no IV should ever be reused with the same key +void AES_CTR_xcrypt_buffer(struct AES_ctx* ctx, uint8_t* buf, size_t length); + +#endif // #if defined(CTR) && (CTR == 1) + + +#endif // _AES_H_
\ No newline at end of file diff --git a/uninstall.sh b/uninstall.sh new file mode 100644 index 0000000..a1acbbf --- /dev/null +++ b/uninstall.sh @@ -0,0 +1,10 @@ +#!/bin/sh -e + +script_dir=$(dirname "$0") +cd "$script_dir" + +[ $(id -u) -ne 0 ] && echo "You need root privileges to run the uninstall script" && exit 1 + +ninja -C build uninstall + +echo "Successfully uninstalled quickmedia"
\ No newline at end of file diff --git a/video_player/jsoncpp b/video_player/jsoncpp -Subproject f23fb32fd9d9c3d01fa67afa0d75f7ff227647e +Subproject 98e28b38ffbae24c0e69e620bf18f8a0c5c6f20 diff --git a/video_player/src/main.cpp b/video_player/src/main.cpp index 54b5bb4..15092d9 100644 --- a/video_player/src/main.cpp +++ b/video_player/src/main.cpp @@ -307,21 +307,27 @@ static bool string_looks_like_double(const char *str, size_t size) { return true; } -static void mpv_set_before_init_options(mpv_handle *mpv_ctx, const Args &args) { +static void set_mpv_properties(mpv_handle *mpv_ctx, const MpvProperty &property, bool is_string_property) { long value_long = 0; double value_double = 0; + if(!is_string_property && string_looks_like_int(property.value.c_str(), property.value.size()) && string_to_long(property.value.c_str(), value_long)) + check_error(mpv_set_option(mpv_ctx, property.key.c_str(), MPV_FORMAT_INT64, &value_long), property.key.c_str()); + else if(!is_string_property && string_looks_like_double(property.value.c_str(), property.value.size()) && string_to_double(property.value.c_str(), value_double)) + check_error(mpv_set_option(mpv_ctx, property.key.c_str(), MPV_FORMAT_DOUBLE, &value_double), property.key.c_str()); + else + check_error(mpv_set_option_string(mpv_ctx, property.key.c_str(), property.value.c_str()), property.key.c_str()); +} - std::set<std::string> known_string_properties = { "start", "force-media-title" }; +static void mpv_set_before_init_options(mpv_handle *mpv_ctx, const Args &args) { + const std::set<std::string> known_string_properties = { "start", "force-media-title" }; + const std::set<std::string> properties_to_ignore = { "profile" }; for(const MpvProperty &property : args.mpv_properties) { - const bool is_string_property = known_string_properties.find(property.key) != known_string_properties.end(); + if(properties_to_ignore.find(property.key) != properties_to_ignore.end()) + continue; - if(!is_string_property && string_looks_like_int(property.value.c_str(), property.value.size()) && string_to_long(property.value.c_str(), value_long)) - check_error(mpv_set_option(mpv_ctx, property.key.c_str(), MPV_FORMAT_INT64, &value_long), property.key.c_str()); - else if(!is_string_property && string_looks_like_double(property.value.c_str(), property.value.size()) && string_to_double(property.value.c_str(), value_double)) - check_error(mpv_set_option(mpv_ctx, property.key.c_str(), MPV_FORMAT_DOUBLE, &value_double), property.key.c_str()); - else - check_error(mpv_set_option_string(mpv_ctx, property.key.c_str(), property.value.c_str()), property.key.c_str()); + const bool is_string_property = known_string_properties.find(property.key) != known_string_properties.end(); + set_mpv_properties(mpv_ctx, property, is_string_property); } if(args.audio_file) { @@ -341,6 +347,19 @@ static void mpv_set_before_init_options(mpv_handle *mpv_ctx, const Args &args) { } } +static void mpv_set_after_init_options(mpv_handle *mpv_ctx, const Args &args) { + const std::set<std::string> known_string_properties = { "profile" }; + const std::set<std::string> properties_to_include = { "profile" }; + + for(const MpvProperty &property : args.mpv_properties) { + if(properties_to_include.find(property.key) == properties_to_include.end()) + continue; + + const bool is_string_property = known_string_properties.find(property.key) != known_string_properties.end(); + set_mpv_properties(mpv_ctx, property, is_string_property); + } +} + #define READ_END 0 #define WRITE_END 1 @@ -473,8 +492,14 @@ static int64_t size_fn(void *cookie) { if(res == 0) { size_t header_start = str_find_case_insensitive(buffer, 0, "200 OK", 6); if(header_start == std::string::npos) { - res = MPV_ERROR_UNSUPPORTED; - goto end; + header_start = str_find_case_insensitive(buffer, 0, "HTTP/3 200", 10); + if(header_start == std::string::npos) + header_start = str_find_case_insensitive(buffer, 0, "HTTP/2 200", 10); + + if(header_start == std::string::npos) { + res = MPV_ERROR_UNSUPPORTED; + goto end; + } } header_start += 6; @@ -624,6 +649,7 @@ int main(int argc, char **argv) { mpv_set_before_init_options(mpv_ctx, args); check_error(mpv_initialize(mpv_ctx), "mpv_initialize"); + mpv_set_after_init_options(mpv_ctx, args); check_error(mpv_stream_cb_add_ro(mpv_ctx, "qm-yt", nullptr, open_fn), "mpv_stream_cb_add_ro"); check_error(mpv_observe_property(mpv_ctx, 0, "idle-active", MPV_FORMAT_FLAG), "observe idle-active"); |