diff --git a/Cargo.lock b/Cargo.lock index a0e23f8..63f7e7c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -269,6 +269,19 @@ dependencies = [ "cc", ] +[[package]] +name = "console" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28b32d32ca44b70c3e4acd7db1babf555fa026e385fb95f18028f88848b3c31" +dependencies = [ + "encode_unicode", + "libc", + "once_cell", + "terminal_size", + "winapi", +] + [[package]] name = "convert_case" version = "0.4.0" @@ -340,6 +353,12 @@ version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + [[package]] name = "encoding" version = "0.2.33" @@ -745,6 +764,18 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "indicatif" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d207dc617c7a380ab07ff572a6e52fa202a2a8f355860ac9c38e23f8196be1b" +dependencies = [ + "console", + "lazy_static", + "number_prefix", + "regex", +] + [[package]] name = "instant" version = "0.1.12" @@ -979,6 +1010,12 @@ dependencies = [ "libc", ] +[[package]] +name = "number_prefix" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" + [[package]] name = "once_cell" version = "1.9.0" @@ -1096,6 +1133,7 @@ name = "pixiv_downloader" version = "0.0.1" dependencies = [ "RustyXML", + "atty", "bindgen", "c_fixed_string", "chrono", @@ -1107,6 +1145,7 @@ dependencies = [ "getopts", "gettext", "html_parser", + "indicatif", "int-enum", "json", "lazy_static", @@ -1518,6 +1557,16 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "terminal_size" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633c1a546cee861a1a6d0dc69ebeca693bf4296661ba7852b9d21d159e0506df" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "textwrap" version = "0.11.0" diff --git a/Cargo.toml b/Cargo.toml index 6a0a1f0..df43220 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +atty = "0.2" c_fixed_string = { version = "0.2", optional = true } chrono = "0.4" dateparser = "0.1.6" @@ -15,9 +16,10 @@ futures-util = "0.3" getopts = "0.2" gettext = "0.4" html_parser = "0.6.2" +indicatif = "0.16" int-enum = { version = "0.4", optional = true } -lazy_static = "1.4" json = "0.12" +lazy_static = "1.4" regex = "1" reqwest = { version = "0.11", features = ["brotli", "deflate", "gzip", "rustls-tls", "socks", "stream"] } RustyXML = "0.3" @@ -33,7 +35,7 @@ cmake = { version = "0.1", optional = true } [features] avdict = ["bindgen", "cmake", "flagset"] exif = ["bindgen", "c_fixed_string", "cmake", "link-cplusplus", "int-enum", "utf16string"] -ugoira = ["avdict", "bindgen", "cmake"] +ugoira = ["avdict", "bindgen", "cmake", "link-cplusplus"] [target.'cfg(windows)'.dependencies] winapi = { version = "0.3.9", features = ["winnls", "stringapiset"] } diff --git a/Language/pixiv_downloader.pot b/Language/pixiv_downloader.pot index 8bf8ce7..9296aae 100644 --- a/Language/pixiv_downloader.pot +++ b/Language/pixiv_downloader.pot @@ -2,7 +2,7 @@ msgid "" msgstr "" "Project-Id-Version: pixiv_downloader\n" -"POT-Creation-Date: 2022-03-16 20:03+0800\n" +"POT-Creation-Date: 2022-03-19 17:08+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -17,12 +17,12 @@ msgstr "" msgid "Failed to parse regex:" msgstr "" -#: author_name_filter.rs:46 retry_interval.rs:22 +#: author_name_filter.rs:46 retry_interval.rs:22 ugoira.rs:280 msgid "Failed to get JSON object." msgstr "" #: author_name_filter.rs:48 author_name_filter.rs:130 retry_interval.rs:61 -#: retry_interval.rs:65 +#: retry_interval.rs:65 ugoira.rs:282 ugoira.rs:287 msgid "Unsupported JSON type." msgstr "" @@ -42,6 +42,22 @@ msgstr "" msgid "Failed parse author name filters:" msgstr "" +#: avdict.rs:44 ugoira.rs:50 +msgid "Failed to decode string with UTF-8:" +msgstr "" + +#: avdict.rs:60 ugoira.rs:106 ugoira.rs:146 ugoira.rs:181 ugoira.rs:270 +msgid "Out of memory." +msgstr "" + +#: avdict.rs:77 ugoira.rs:158 ugoira.rs:204 +msgid "Failed to get error message:" +msgstr "" + +#: avdict.rs:169 +msgid "Failed to get value for entry." +msgstr "" + #: cookies.rs:75 cookies.rs:217 msgid "Warning: Failed to parse URL:" msgstr "" @@ -98,15 +114,15 @@ msgstr "" msgid "Can not parse expired time:" msgstr "" -#: cookies.rs:366 data/json.rs:53 settings.rs:418 webclient.rs:283 +#: cookies.rs:366 data/json.rs:50 settings.rs:418 webclient.rs:302 msgid "Failed to remove file:" msgstr "" -#: cookies.rs:372 data/json.rs:64 settings.rs:425 webclient.rs:289 +#: cookies.rs:372 data/json.rs:61 settings.rs:425 webclient.rs:312 msgid "Failed to create file:" msgstr "" -#: cookies.rs:379 data/json.rs:70 settings.rs:431 webclient.rs:302 +#: cookies.rs:379 data/json.rs:67 settings.rs:431 webclient.rs:329 msgid "Failed to write file:" msgstr "" @@ -114,44 +130,84 @@ msgstr "" msgid "Failed to unescape string:" msgstr "" -#: download.rs:19 pixiv_web.rs:58 +#: download.rs:25 pixiv_web.rs:58 msgid "Failed to initialize pixiv web api client." msgstr "" -#: download.rs:28 +#: download.rs:34 msgid "Warning: Web api client not logined, some future may not work." msgstr "" -#: download.rs:64 +#: download.rs:70 msgid "Failed to get page count." msgstr "" -#: download.rs:73 +#: download.rs:79 msgid "Failed to get pages' data." msgstr "" -#: download.rs:86 +#: download.rs:92 msgid "Failed to save metadata to JSON file." msgstr "" -#: download.rs:96 download.rs:170 -msgid "Failed to get original picture's link." +#: download.rs:107 +msgid "Failed to get ugoira's data." msgstr "" -#: download.rs:102 download.rs:176 +#: download.rs:113 +msgid "Can not find source link for ugoira." +msgstr "" + +#: download.rs:119 download.rs:197 download.rs:271 msgid "Failed to get file name from url:" msgstr "" -#: download.rs:117 download.rs:156 download.rs:195 download.rs:225 +#: download.rs:135 download.rs:141 +msgid "Failed to download ugoira:" +msgstr "" + +#: download.rs:146 +msgid "Downloaded ugoira:" +msgstr "" + +#: download.rs:156 +msgid "Warning: Failed to generate video's metadata:" +msgstr "" + +#: download.rs:166 +msgid "Failed to convert from ugoira to mp4 video file:" +msgstr "" + +#: download.rs:169 +msgid "Converted -> " +msgstr "" + +#: download.rs:172 +msgid "Failed to parse frames:" +msgstr "" + +#: download.rs:179 +msgid "Warning: Unknown illust type:" +msgstr "" + +#: download.rs:182 +msgid "Warning: Failed to get illust's type." +msgstr "" + +#: download.rs:191 download.rs:265 +msgid "Failed to get original picture's link." +msgstr "" + +#: download.rs:212 download.rs:251 download.rs:291 download.rs:321 msgid "Failed to add exif data to image:" msgstr "" -#: download.rs:136 download.rs:142 download.rs:205 download.rs:211 +#: download.rs:231 download.rs:237 download.rs:301 download.rs:307 #: pixiv_web.rs:153 msgid "Failed to download image:" msgstr "" -#: download.rs:147 download.rs:216 +#: download.rs:242 download.rs:312 msgid "Downloaded image:" msgstr "" @@ -159,95 +215,95 @@ msgstr "" msgid "Failed to parse duration from string." msgstr "" -#: opts.rs:91 +#: opts.rs:93 msgid "Warning: The specified config file not found." msgstr "" -#: opts.rs:115 +#: opts.rs:117 msgid "Usage:" msgstr "" -#: opts.rs:117 +#: opts.rs:119 msgid "Download an artwork" msgstr "" -#: opts.rs:119 +#: opts.rs:121 msgid "Fix the config file" msgstr "" -#: opts.rs:121 +#: opts.rs:123 msgid "Print all available settings" msgstr "" -#: opts.rs:129 +#: opts.rs:131 msgid "Print help message." msgstr "" -#: opts.rs:133 +#: opts.rs:135 msgid "The location of config file." msgstr "" -#: opts.rs:139 settings_list.rs:11 +#: opts.rs:141 settings_list.rs:11 msgid "The location of cookies file. Used for web API." msgstr "" -#: opts.rs:145 settings_list.rs:12 +#: opts.rs:147 settings_list.rs:12 msgid "The language of translated tags." msgstr "" -#: opts.rs:148 +#: opts.rs:150 msgid "Verbose logging." msgstr "" -#: opts.rs:149 +#: opts.rs:151 msgid "Overwrite existing file." msgstr "" -#: opts.rs:150 +#: opts.rs:152 msgid "Skip overwrite existing file." msgstr "" -#: opts.rs:154 settings_list.rs:13 +#: opts.rs:156 settings_list.rs:13 msgid "Max retry count if request failed." msgstr "" -#: opts.rs:160 settings_list.rs:14 +#: opts.rs:162 settings_list.rs:14 msgid "The interval (in seconds) between two retries." msgstr "" -#: opts.rs:163 settings_list.rs:15 +#: opts.rs:165 settings_list.rs:15 msgid "Use data from webpage first." msgstr "" -#: opts.rs:167 settings_list.rs:18 +#: opts.rs:170 settings_list.rs:18 msgid "Add/Update exif information to image files even when overwrite are disabled." msgstr "" -#: opts.rs:188 +#: opts.rs:191 msgid "Unknown command." msgstr "" -#: opts.rs:198 +#: opts.rs:201 msgid "Failed to parse ID:" msgstr "" -#: opts.rs:204 +#: opts.rs:207 msgid "No URL or ID specified." msgstr "" -#: opts.rs:212 +#: opts.rs:215 msgid "No detailed command specified." msgstr "" -#: opts.rs:225 +#: opts.rs:228 msgid "Unknown config subcommand." msgstr "" -#: opts.rs:261 +#: opts.rs:264 msgid "Retry count must be an non-negative integer:" msgstr "" -#: opts.rs:270 +#: opts.rs:273 msgid "Failed to parse retry interval:" msgstr "" @@ -303,7 +359,11 @@ msgstr "" msgid "Artwork's page data:" msgstr "" -#: pixiv_web.rs:236 +#: pixiv_web.rs:226 +msgid "Ugoira's data:" +msgstr "" + +#: pixiv_web.rs:250 msgid "Warning: Failed to save cookies file:" msgstr "" @@ -355,11 +415,75 @@ msgstr "" msgid "Remove the part which after these parttens." msgstr "" +#: ugoira.rs:101 +msgid "Arguments have null pointers." +msgstr "" + +#: ugoira.rs:102 +msgid "Invalid max fps." +msgstr "" + +#: ugoira.rs:103 +msgid "Invalid frames." +msgstr "" + +#: ugoira.rs:104 +msgid "Invalid crf." +msgstr "" + +#: ugoira.rs:105 +msgid "Can not remove output file." +msgstr "" + +#: ugoira.rs:107 +msgid "No video stream available in the file." +msgstr "" + +#: ugoira.rs:108 +msgid "No available decoder." +msgstr "" + +#: ugoira.rs:109 +msgid "No available encoder." +msgstr "" + +#: ugoira.rs:110 +msgid "Failed to open output file." +msgstr "" + +#: ugoira.rs:111 +msgid "Unable to scale image." +msgstr "" + +#: ugoira.rs:112 +msgid "Unknown error." +msgstr "" + +#: ugoira.rs:177 +msgid "No error." +msgstr "" + +#: ugoira.rs:266 +msgid "Delay should be greater than 0." +msgstr "" + +#: ugoira.rs:289 +msgid "File is needed." +msgstr "" + +#: ugoira.rs:290 +msgid "Delay is needed." +msgstr "" + +#: ugoira.rs:350 ugoira.rs:351 +msgid "Failed to convert path." +msgstr "" + #: utils.rs:30 msgid "Do you want to delete file \"\"?" msgstr "" -#: utils.rs:51 webclient.rs:151 +#: utils.rs:51 webclient.rs:154 msgid "Can not parse URL:" msgstr "" @@ -367,50 +491,54 @@ msgstr "" msgid "Failed to get file name from path:" msgstr "" -#: webclient.rs:111 +#: webclient.rs:114 msgid "Failed to parse Set-Cookie header." msgstr "" -#: webclient.rs:116 +#: webclient.rs:119 msgid "Failed to convert to string:" msgstr "" -#: webclient.rs:158 +#: webclient.rs:161 msgid "Parameters should be object or array:" msgstr "" -#: webclient.rs:178 +#: webclient.rs:181 msgid "Parameters should be array:" msgstr "" -#: webclient.rs:182 +#: webclient.rs:185 msgid "Parameters need at least a value:" msgstr "" -#: webclient.rs:221 +#: webclient.rs:224 msgid "Retry after seconds." msgstr "" -#: webclient.rs:225 +#: webclient.rs:228 msgid "Retry times now." msgstr "" -#: webclient.rs:239 +#: webclient.rs:242 msgid "Error when request:" msgstr "" -#: webclient.rs:296 +#: webclient.rs:308 +msgid "Downloading \"\"." +msgstr "" + +#: webclient.rs:319 msgid "Error when downloading file:" msgstr "" -#: main.rs:71 +#: main.rs:84 msgid "Failed to save config file:" msgstr "" -#: main.rs:82 +#: main.rs:95 msgid "All available settings:" msgstr "" -#: main.rs:114 +#: main.rs:127 msgid "Can not read config file:" msgstr "" diff --git a/Language/pixiv_downloader.zh_CN.po b/Language/pixiv_downloader.zh_CN.po index 2717394..2926d58 100644 --- a/Language/pixiv_downloader.zh_CN.po +++ b/Language/pixiv_downloader.zh_CN.po @@ -1,8 +1,8 @@ msgid "" msgstr "" "Project-Id-Version: pixiv_downloader\n" -"POT-Creation-Date: 2022-03-16 20:03+0800\n" -"PO-Revision-Date: 2022-03-16 20:05+0800\n" +"POT-Creation-Date: 2022-03-19 17:08+0800\n" +"PO-Revision-Date: 2022-03-19 17:08+0800\n" "Last-Translator: lifegpc \n" "Language-Team: \n" "Language: zh_CN\n" @@ -18,12 +18,12 @@ msgstr "" msgid "Failed to parse regex:" msgstr "无法解析正则:" -#: author_name_filter.rs:46 retry_interval.rs:22 +#: author_name_filter.rs:46 retry_interval.rs:22 ugoira.rs:280 msgid "Failed to get JSON object." msgstr "无法获取 JSON 对象。" #: author_name_filter.rs:48 author_name_filter.rs:130 retry_interval.rs:61 -#: retry_interval.rs:65 +#: retry_interval.rs:65 ugoira.rs:282 ugoira.rs:287 msgid "Unsupported JSON type." msgstr "不支持的 JSON 类型。" @@ -43,6 +43,22 @@ msgstr "未知的过滤器类型:" msgid "Failed parse author name filters:" msgstr "无法解析作者名称过滤器:" +#: avdict.rs:44 ugoira.rs:50 +msgid "Failed to decode string with UTF-8:" +msgstr "无法用 UTF-8 解码字符串:" + +#: avdict.rs:60 ugoira.rs:106 ugoira.rs:146 ugoira.rs:181 ugoira.rs:270 +msgid "Out of memory." +msgstr "内存溢出。" + +#: avdict.rs:77 ugoira.rs:158 ugoira.rs:204 +msgid "Failed to get error message:" +msgstr "无法获取错误消息:" + +#: avdict.rs:169 +msgid "Failed to get value for entry." +msgstr "无法获取条目的值。" + #: cookies.rs:75 cookies.rs:217 msgid "Warning: Failed to parse URL:" msgstr "警告:无法解析 URL:" @@ -99,15 +115,15 @@ msgstr "无效的Cookie:" msgid "Can not parse expired time:" msgstr "无法解析过期时间:" -#: cookies.rs:366 data/json.rs:53 settings.rs:418 webclient.rs:283 +#: cookies.rs:366 data/json.rs:50 settings.rs:418 webclient.rs:302 msgid "Failed to remove file:" msgstr "无法删除文件:" -#: cookies.rs:372 data/json.rs:64 settings.rs:425 webclient.rs:289 +#: cookies.rs:372 data/json.rs:61 settings.rs:425 webclient.rs:312 msgid "Failed to create file:" msgstr "无法创建文件:" -#: cookies.rs:379 data/json.rs:70 settings.rs:431 webclient.rs:302 +#: cookies.rs:379 data/json.rs:67 settings.rs:431 webclient.rs:329 msgid "Failed to write file:" msgstr "无法写入文件:" @@ -115,44 +131,84 @@ msgstr "无法写入文件:" msgid "Failed to unescape string:" msgstr "无法反转义字符串:" -#: download.rs:19 pixiv_web.rs:58 +#: download.rs:25 pixiv_web.rs:58 msgid "Failed to initialize pixiv web api client." msgstr "无法初始化 Pixiv 网页 API 客户端。" -#: download.rs:28 +#: download.rs:34 msgid "Warning: Web api client not logined, some future may not work." msgstr "警告:Web API 客户端未登录,一些功能可能无法工作。" -#: download.rs:64 +#: download.rs:70 msgid "Failed to get page count." msgstr "无法获取页数。" -#: download.rs:73 +#: download.rs:79 msgid "Failed to get pages' data." msgstr "无法获取每页数据。" -#: download.rs:86 +#: download.rs:92 msgid "Failed to save metadata to JSON file." msgstr "无法将元数据保存到 JSON 文件。" -#: download.rs:96 download.rs:170 -msgid "Failed to get original picture's link." -msgstr "无法获取原图链接。" +#: download.rs:107 +msgid "Failed to get ugoira's data." +msgstr "无法获取动图数据。" -#: download.rs:102 download.rs:176 +#: download.rs:113 +msgid "Can not find source link for ugoira." +msgstr "无法获取动图的链接。" + +#: download.rs:119 download.rs:197 download.rs:271 msgid "Failed to get file name from url:" msgstr "无法从 URL 获取文件名:" -#: download.rs:117 download.rs:156 download.rs:195 download.rs:225 +#: download.rs:135 download.rs:141 +msgid "Failed to download ugoira:" +msgstr "无法下载动图:" + +#: download.rs:146 +msgid "Downloaded ugoira:" +msgstr "已下载动图:" + +#: download.rs:156 +msgid "Warning: Failed to generate video's metadata:" +msgstr "警告:无法生成视频元数据:" + +#: download.rs:166 +msgid "Failed to convert from ugoira to mp4 video file:" +msgstr "无法将动图转换为 mp4 视频文件:" + +#: download.rs:169 +msgid "Converted -> " +msgstr "已转换 -> " + +#: download.rs:172 +msgid "Failed to parse frames:" +msgstr "无法解析帧数据:" + +#: download.rs:179 +msgid "Warning: Unknown illust type:" +msgstr "警告:未知的插图类型:" + +#: download.rs:182 +msgid "Warning: Failed to get illust's type." +msgstr "警告:无法获取插图类型。" + +#: download.rs:191 download.rs:265 +msgid "Failed to get original picture's link." +msgstr "无法获取原图链接。" + +#: download.rs:212 download.rs:251 download.rs:291 download.rs:321 msgid "Failed to add exif data to image:" msgstr "无法往图片增加 EXIF 数据:" -#: download.rs:136 download.rs:142 download.rs:205 download.rs:211 +#: download.rs:231 download.rs:237 download.rs:301 download.rs:307 #: pixiv_web.rs:153 msgid "Failed to download image:" msgstr "无法下载图片:" -#: download.rs:147 download.rs:216 +#: download.rs:242 download.rs:312 msgid "Downloaded image:" msgstr "已下载图片:" @@ -160,96 +216,96 @@ msgstr "已下载图片:" msgid "Failed to parse duration from string." msgstr "无法解析从字符串解析时长。" -#: opts.rs:91 +#: opts.rs:93 msgid "Warning: The specified config file not found." msgstr "警告:没有找到指定的设置文件。" -#: opts.rs:115 +#: opts.rs:117 msgid "Usage:" msgstr "使用方法:" -#: opts.rs:117 +#: opts.rs:119 msgid "Download an artwork" msgstr "下载一个作品" -#: opts.rs:119 +#: opts.rs:121 msgid "Fix the config file" msgstr "修复设置文件" -#: opts.rs:121 +#: opts.rs:123 msgid "Print all available settings" msgstr "打印所有可用的设置" -#: opts.rs:129 +#: opts.rs:131 msgid "Print help message." msgstr "打印帮助信息。" -#: opts.rs:133 +#: opts.rs:135 msgid "The location of config file." msgstr "设置文件的位置。" -#: opts.rs:139 settings_list.rs:11 +#: opts.rs:141 settings_list.rs:11 msgid "The location of cookies file. Used for web API." msgstr "cookies 文件的位置。用于网页 API。" -#: opts.rs:145 settings_list.rs:12 +#: opts.rs:147 settings_list.rs:12 msgid "The language of translated tags." msgstr "翻译后的标签语言。" -#: opts.rs:148 +#: opts.rs:150 msgid "Verbose logging." msgstr "启用详细内容输出。" -#: opts.rs:149 +#: opts.rs:151 msgid "Overwrite existing file." msgstr "覆盖已有文件。" -#: opts.rs:150 +#: opts.rs:152 msgid "Skip overwrite existing file." msgstr "跳过覆盖已有文件。" -#: opts.rs:154 settings_list.rs:13 +#: opts.rs:156 settings_list.rs:13 msgid "Max retry count if request failed." msgstr "请求失败时最大重试次数。" -#: opts.rs:160 settings_list.rs:14 +#: opts.rs:162 settings_list.rs:14 msgid "The interval (in seconds) between two retries." msgstr "两次尝试的间隔时间(单位:秒)。" -#: opts.rs:163 settings_list.rs:15 +#: opts.rs:165 settings_list.rs:15 msgid "Use data from webpage first." msgstr "优先使用来自网页的数据。" -#: opts.rs:167 settings_list.rs:18 +#: opts.rs:170 settings_list.rs:18 msgid "" "Add/Update exif information to image files even when overwrite are disabled." msgstr "添加或更新图片的 EXIF 信息,即使不覆盖图片文件。" -#: opts.rs:188 +#: opts.rs:191 msgid "Unknown command." msgstr "未知指令。" -#: opts.rs:198 +#: opts.rs:201 msgid "Failed to parse ID:" msgstr "无法解析 ID:" -#: opts.rs:204 +#: opts.rs:207 msgid "No URL or ID specified." msgstr "没有指定网址或 ID。" -#: opts.rs:212 +#: opts.rs:215 msgid "No detailed command specified." msgstr "没有指定更详细的指令。" -#: opts.rs:225 +#: opts.rs:228 msgid "Unknown config subcommand." msgstr "未知的 config 子指令。" -#: opts.rs:261 +#: opts.rs:264 msgid "Retry count must be an non-negative integer:" msgstr "重试次数不能是负数:" -#: opts.rs:270 +#: opts.rs:273 msgid "Failed to parse retry interval:" msgstr "无法解析间隔时间:" @@ -305,7 +361,11 @@ msgstr "无法解析作品页。" msgid "Artwork's page data:" msgstr "作品页面数据:" -#: pixiv_web.rs:236 +#: pixiv_web.rs:226 +msgid "Ugoira's data:" +msgstr "动图数据:" + +#: pixiv_web.rs:250 msgid "Warning: Failed to save cookies file:" msgstr "警告:无法保存 cookies 文件:" @@ -361,11 +421,75 @@ msgstr "Pixiv 的 refresh tokens。用于登录。" msgid "Remove the part which after these parttens." msgstr "移除匹配的部分。" +#: ugoira.rs:101 +msgid "Arguments have null pointers." +msgstr "参数包含空指针。" + +#: ugoira.rs:102 +msgid "Invalid max fps." +msgstr "无效的最大帧率。" + +#: ugoira.rs:103 +msgid "Invalid frames." +msgstr "无效的帧信息。" + +#: ugoira.rs:104 +msgid "Invalid crf." +msgstr "无效的 CRF。" + +#: ugoira.rs:105 +msgid "Can not remove output file." +msgstr "无法删除输出文件。" + +#: ugoira.rs:107 +msgid "No video stream available in the file." +msgstr "文件中不存在视频流。" + +#: ugoira.rs:108 +msgid "No available decoder." +msgstr "没有可用的解码器。" + +#: ugoira.rs:109 +msgid "No available encoder." +msgstr "没有可用的编码器。" + +#: ugoira.rs:110 +msgid "Failed to open output file." +msgstr "无法打开输出文件。" + +#: ugoira.rs:111 +msgid "Unable to scale image." +msgstr "无法处理图片。" + +#: ugoira.rs:112 +msgid "Unknown error." +msgstr "未知错误。" + +#: ugoira.rs:177 +msgid "No error." +msgstr "无错误。" + +#: ugoira.rs:266 +msgid "Delay should be greater than 0." +msgstr "延迟需要大于 0 。" + +#: ugoira.rs:289 +msgid "File is needed." +msgstr "需要文件。" + +#: ugoira.rs:290 +msgid "Delay is needed." +msgstr "需要延迟。" + +#: ugoira.rs:350 ugoira.rs:351 +msgid "Failed to convert path." +msgstr "无法转换路径。" + #: utils.rs:30 msgid "Do you want to delete file \"\"?" msgstr "你想要删除文件 吗?" -#: utils.rs:51 webclient.rs:151 +#: utils.rs:51 webclient.rs:154 msgid "Can not parse URL:" msgstr "无法解析 URL:" @@ -373,50 +497,54 @@ msgstr "无法解析 URL:" msgid "Failed to get file name from path:" msgstr "无法从路径获取文件名:" -#: webclient.rs:111 +#: webclient.rs:114 msgid "Failed to parse Set-Cookie header." msgstr "无法解析 Set-Cookie 头部。" -#: webclient.rs:116 +#: webclient.rs:119 msgid "Failed to convert to string:" msgstr "无法转换为字符串:" -#: webclient.rs:158 +#: webclient.rs:161 msgid "Parameters should be object or array:" msgstr "参数应该是对象或数组:" -#: webclient.rs:178 +#: webclient.rs:181 msgid "Parameters should be array:" msgstr "参数应该是数组:" -#: webclient.rs:182 +#: webclient.rs:185 msgid "Parameters need at least a value:" msgstr "参数需要至少含有一个值:" -#: webclient.rs:221 +#: webclient.rs:224 msgid "Retry after seconds." msgstr " 秒后重试。" -#: webclient.rs:225 +#: webclient.rs:228 msgid "Retry times now." msgstr "正在进行第 次重试。" -#: webclient.rs:239 +#: webclient.rs:242 msgid "Error when request:" msgstr "请求时发生错误:" -#: webclient.rs:296 +#: webclient.rs:308 +msgid "Downloading \"\"." +msgstr "正在下载 \"\"。" + +#: webclient.rs:319 msgid "Error when downloading file:" msgstr "下载文件时发生错误:" -#: main.rs:71 +#: main.rs:84 msgid "Failed to save config file:" msgstr "无法保存设置文件:" -#: main.rs:82 +#: main.rs:95 msgid "All available settings:" msgstr "所有可用的设置:" -#: main.rs:114 +#: main.rs:127 msgid "Can not read config file:" msgstr "无法读取设置文件:" diff --git a/build.rs b/build.rs index 6f943a6..197061e 100644 --- a/build.rs +++ b/build.rs @@ -3,19 +3,19 @@ extern crate bindgen; #[cfg(feature = "cmake")] extern crate cmake; -#[cfg(any(feature = "avdict", feature = "exif"))] +#[cfg(any(feature = "avdict", feature = "exif", feature = "ugoira"))] use std::env; -#[cfg(any(feature = "avdict", feature = "exif"))] +#[cfg(any(feature = "avdict", feature = "exif", feature = "ugoira"))] use std::fs::create_dir; -#[cfg(any(feature = "avdict", feature = "exif"))] +#[cfg(any(feature = "avdict", feature = "exif", feature = "ugoira"))] use std::fs::File; -#[cfg(any(feature = "avdict", feature = "exif"))] +#[cfg(any(feature = "avdict", feature = "exif", feature = "ugoira"))] use std::io::Read; -#[cfg(any(feature = "avdict", feature = "exif"))] +#[cfg(any(feature = "avdict", feature = "exif", feature = "ugoira"))] use std::path::PathBuf; fn main() { - #[cfg(any(feature = "exif"))] + #[cfg(any(feature = "exif", feature = "ugoira"))] { let out_path = PathBuf::from(env::var("OUT_DIR").unwrap()); let utils_build_path = out_path.join("utils"); @@ -145,4 +145,52 @@ fn main() { .write_to_file(out_path.join("exif.rs")) .expect("Couldn't write bindings!"); } + #[cfg(feature = "ugoira")] + { + let out_path = PathBuf::from(env::var("OUT_DIR").unwrap()); + let ugoira_build_path = out_path.join("ugoira"); + let utils_build_path = out_path.join("utils"); + if !ugoira_build_path.exists() { + create_dir(&ugoira_build_path).unwrap(); + } + cmake::Config::new("ugoira") + .define("CMAKE_INSTALL_PREFIX", out_path.to_str().unwrap()) + .out_dir(ugoira_build_path) + .define("UTILS_LIBRARY", utils_build_path.to_str().unwrap()) + .define("CMAKE_BUILD_TYPE", "Release") + .build(); + println!("cargo:rustc-link-search=native={}/lib", out_path.display()); + let dep_path = out_path.join("ugoira_dep.txt"); + let mut f = File::open(dep_path).unwrap(); + let mut s = String::from(""); + f.read_to_string(&mut s).unwrap(); + println!("cargo:rustc-link-lib=static=ugoira"); + let l: Vec<&str> = s.split(";").collect(); + for i in l.iter() { + let mut p = PathBuf::from(i); + let p2 = p.clone(); + let file_name = p2.file_stem().unwrap(); + let file_name = file_name.to_str().unwrap(); + let file_name = file_name.trim_start_matches("lib"); + p.pop(); + println!("cargo:rustc-link-search={}", p.to_str().unwrap()); + println!("cargo:rustc-link-lib={}", file_name); + } + println!("cargo:rerun-if-changed=ugoira/"); + let bindings = bindgen::Builder::default() + // The input header we would like to generate + // bindings for. + .header("ugoira/ugoira.h") + // Tell cargo to invalidate the built crate whenever any of the + // included header files changed. + .parse_callbacks(Box::new(bindgen::CargoCallbacks)) + // Finish the builder and generate the bindings. + .generate() + // Unwrap the Result and panic on failure. + .expect("Unable to generate bindings"); + + bindings + .write_to_file(out_path.join("ugoira.rs")) + .expect("Couldn't write bindings!"); + } } diff --git a/cmake/FindAVCODEC.cmake b/cmake/FindAVCODEC.cmake new file mode 100644 index 0000000..ff66ab4 --- /dev/null +++ b/cmake/FindAVCODEC.cmake @@ -0,0 +1,29 @@ +find_package(PkgConfig) +if (PkgConfig_FOUND) + pkg_check_modules(PC_AVCODEC QUIET IMPORTED_TARGET GLOBAL libavcodec) +endif() + +if (PC_AVCODEC_FOUND) + set(AVCODEC_FOUND TRUE) + set(AVCODEC_VERSION ${PC_AVCODEC_VERSION}) + set(AVCODEC_VERSION_STRING ${PC_AVCODEC_STRING}) + set(AVCODEC_LIBRARYS ${PC_AVCODEC_LIBRARIES}) + if (USE_STATIC_LIBS) + set(AVCODEC_INCLUDE_DIRS ${PC_AVCODEC_STATIC_INCLUDE_DIRS}) + else() + set(AVCODEC_INCLUDE_DIRS ${PC_AVCODEC_INCLUDE_DIRS}) + endif() + if (NOT TARGET AVCODEC::AVCODEC) + add_library(AVCODEC::AVCODEC ALIAS PkgConfig::PC_AVCODEC) + endif() +else() + message(FATAL_ERROR "failed to find libavcodec.") +endif() + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(AVCODEC + FOUND_VAR AVCODEC_FOUND + REQUIRED_VARS + AVCODEC_LIBRARYS + VERSION_VAR AVCODEC_VERSION +) diff --git a/cmake/FindAVFORMAT.cmake b/cmake/FindAVFORMAT.cmake new file mode 100644 index 0000000..c3f39d1 --- /dev/null +++ b/cmake/FindAVFORMAT.cmake @@ -0,0 +1,29 @@ +find_package(PkgConfig) +if (PkgConfig_FOUND) + pkg_check_modules(PC_AVFORMAT QUIET IMPORTED_TARGET GLOBAL libavformat) +endif() + +if (PC_AVFORMAT_FOUND) + set(AVFORMAT_FOUND TRUE) + set(AVFORMAT_VERSION ${PC_AVFORMAT_VERSION}) + set(AVFORMAT_VERSION_STRING ${PC_AVFORMAT_STRING}) + set(AVFORMAT_LIBRARYS ${PC_AVFORMAT_LIBRARIES}) + if (USE_STATIC_LIBS) + set(AVFORMAT_INCLUDE_DIRS ${PC_AVFORMAT_STATIC_INCLUDE_DIRS}) + else() + set(AVFORMAT_INCLUDE_DIRS ${PC_AVFORMAT_INCLUDE_DIRS}) + endif() + if (NOT TARGET AVFORMAT::AVFORMAT) + add_library(AVFORMAT::AVFORMAT ALIAS PkgConfig::PC_AVFORMAT) + endif() +else() + message(FATAL_ERROR "failed to find libavformat.") +endif() + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(AVFORMAT + FOUND_VAR AVFORMAT_FOUND + REQUIRED_VARS + AVFORMAT_LIBRARYS + VERSION_VAR AVFORMAT_VERSION +) diff --git a/cmake/FindLIBZIP.cmake b/cmake/FindLIBZIP.cmake new file mode 100644 index 0000000..9e9537f --- /dev/null +++ b/cmake/FindLIBZIP.cmake @@ -0,0 +1,29 @@ +find_package(PkgConfig) +if (PkgConfig_FOUND) + pkg_check_modules(PC_LIBZIP QUIET IMPORTED_TARGET GLOBAL libzip) +endif() + +if (PC_LIBZIP_FOUND) + set(LIBZIP_FOUND TRUE) + set(LIBZIP_VERSION ${PC_LIBZIP_VERSION}) + set(LIBZIP_VERSION_STRING ${PC_LIBZIP_STRING}) + set(LIBZIP_LIBRARYS ${PC_LIBZIP_LIBRARIES}) + if (USE_STATIC_LIBS) + set(LIBZIP_INCLUDE_DIRS ${PC_LIBZIP_STATIC_INCLUDE_DIRS}) + else() + set(LIBZIP_INCLUDE_DIRS ${PC_LIBZIP_INCLUDE_DIRS}) + endif() + if (NOT TARGET LIBZIP::LIBZIP) + add_library(LIBZIP::LIBZIP ALIAS PkgConfig::PC_LIBZIP) + endif() +else() + message(FATAL_ERROR "failed.") +endif() + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(LIBZIP + FOUND_VAR LIBZIP_FOUND + REQUIRED_VARS + LIBZIP_LIBRARYS + VERSION_VAR LIBZIP_VERSION +) diff --git a/cmake/FindSWSCALE.cmake b/cmake/FindSWSCALE.cmake new file mode 100644 index 0000000..b5634c3 --- /dev/null +++ b/cmake/FindSWSCALE.cmake @@ -0,0 +1,29 @@ +find_package(PkgConfig) +if (PkgConfig_FOUND) + pkg_check_modules(PC_SWSCALE QUIET IMPORTED_TARGET GLOBAL libswscale) +endif() + +if (PC_SWSCALE_FOUND) + set(SWSCALE_FOUND TRUE) + set(SWSCALE_VERSION ${PC_SWSCALE_VERSION}) + set(SWSCALE_VERSION_STRING ${PC_SWSCALE_STRING}) + set(SWSCALE_LIBRARYS ${PC_SWSCALE_LIBRARIES}) + if (USE_STATIC_LIBS) + set(SWSCALE_INCLUDE_DIRS ${PC_SWSCALE_STATIC_INCLUDE_DIRS}) + else() + set(SWSCALE_INCLUDE_DIRS ${PC_SWSCALE_INCLUDE_DIRS}) + endif() + if (NOT TARGET SWSCALE::SWSCALE) + add_library(SWSCALE::SWSCALE ALIAS PkgConfig::PC_SWSCALE) + endif() +else() + message(FATAL_ERROR "failed to find libswscale.") +endif() + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(SWSCALE + FOUND_VAR SWSCALE_FOUND + REQUIRED_VARS + SWSCALE_LIBRARYS + VERSION_VAR SWSCALE_VERSION +) diff --git a/cmake/GetLinkLibraries.cmake b/cmake/GetLinkLibraries.cmake index 50c746a..da977bb 100644 --- a/cmake/GetLinkLibraries.cmake +++ b/cmake/GetLinkLibraries.cmake @@ -9,20 +9,25 @@ function(get_link_libraries OUTPUT_LIST TARGET) set(LIB_FILES "") foreach(LIB ${LIBS}) if (TARGET ${LIB}) + get_target_property(LIB_IMPORTED ${LIB} IMPORTED) list(FIND VISITED_TARGETS ${LIB} VISITED) if (${VISITED} EQUAL -1) - get_target_property(LIB_FILE ${LIB} LOCATION) - if (NOT LIB_FILE) - get_target_property(LIB_FILE ${LIB} IMPORTED_LOCATION) + if (LIB_IMPORTED) + get_target_property(LIB_FILE ${LIB} LOCATION) + if (NOT LIB_FILE) + get_target_property(LIB_FILE ${LIB} IMPORTED_LOCATION) + endif() + if (NOT LIB_FILE) + get_target_property(LIB_FILE ${LIB} IMPORTED_IMPLIB) + endif() + if (NOT LIB_FILE) + get_target_property(LIB_FILE ${LIB} INTERFACE_LINK_LIBRARIES) + endif() + get_link_libraries(LINK_LIB_FILES ${LIB}) + list(APPEND LIB_FILES ${LIB_FILE} ${LINK_LIB_FILES}) + else() + list(APPEND LIB_FILES ${LIB}) endif() - if (NOT LIB_FILE) - get_target_property(LIB_FILE ${LIB} IMPORTED_IMPLIB) - endif() - if (NOT LIB_FILE) - get_target_property(LIB_FILE ${LIB} INTERFACE_LINK_LIBRARIES) - endif() - get_link_libraries(LINK_LIB_FILES ${LIB}) - list(APPEND LIB_FILES ${LIB_FILE} ${LINK_LIB_FILES}) endif() endif() endforeach() diff --git a/src/_ugoira.rs b/src/_ugoira.rs new file mode 100644 index 0000000..254c08b --- /dev/null +++ b/src/_ugoira.rs @@ -0,0 +1,6 @@ +#![allow(non_camel_case_types)] +#![allow(non_snake_case)] +#![allow(dead_code)] +#![allow(deref_nullptr)] +#![allow(non_upper_case_globals)] +include!(concat!(env!("OUT_DIR"), "/ugoira.rs")); diff --git a/src/author_name_filter.rs b/src/author_name_filter.rs index b98a154..dedcab9 100644 --- a/src/author_name_filter.rs +++ b/src/author_name_filter.rs @@ -1,4 +1,4 @@ -use crate::data::json::ToJson; +use crate::ext::json::ToJson; use crate::gettext; use crate::stdext::TryErr; use json::JsonValue; diff --git a/src/avdict.rs b/src/avdict.rs index c271975..0d80c1d 100644 --- a/src/avdict.rs +++ b/src/avdict.rs @@ -42,8 +42,8 @@ impl Display for AVDictError { match self { Self::String(s) => { f.write_str(s) } Self::Utf8Error(s) => { f.write_fmt(format_args!("{} {}", gettext("Failed to decode string with UTF-8:"), s)) } - Self::CodeError(s) => { s.fmt(f) } - Self::ToCstr(s) => { s.fmt(f) } + Self::CodeError(s) => { f.write_fmt(format_args!("{}", s)) } + Self::ToCstr(s) => { f.write_fmt(format_args!("{}", s)) } } } } @@ -109,7 +109,7 @@ flagset::flags! { } pub struct AVDict { - m: *mut _avdict::AVDict, + pub m: *mut _avdict::AVDict, } #[allow(dead_code)] diff --git a/src/data/json.rs b/src/data/json.rs index 015bf49..0dad2f1 100644 --- a/src/data/json.rs +++ b/src/data/json.rs @@ -1,4 +1,5 @@ use crate::data::data::PixivData; +use crate::ext::json::ToJson; use crate::gettext; use crate::parser::description::parse_description; use crate::pixiv_link::PixivID; @@ -12,10 +13,6 @@ use std::fs::remove_file; use std::io::Write; use std::path::Path; -pub trait ToJson { - fn to_json(&self) -> Option; -} - /// Store metadata informations in JSON file pub struct JSONDataFile { id: PixivID, @@ -117,33 +114,3 @@ impl ToJson for JSONDataFile { Some(value) } } - -impl ToJson for &str { - fn to_json(&self) -> Option { - Some(JsonValue::String(String::from(*self))) - } -} - -impl ToJson for &String { - fn to_json(&self) -> Option { - Some(JsonValue::String((*self).to_string())) - } -} - -impl ToJson for String { - fn to_json(&self) -> Option { - Some(JsonValue::String(self.to_string())) - } -} - -impl ToJson for JsonValue { - fn to_json(&self) -> Option { - Some(self.clone()) - } -} - -impl ToJson for &JsonValue { - fn to_json(&self) -> Option { - Some((*self).clone()) - } -} diff --git a/src/data/mod.rs b/src/data/mod.rs index cc06fe7..aa85c4e 100644 --- a/src/data/mod.rs +++ b/src/data/mod.rs @@ -2,3 +2,5 @@ pub mod data; #[cfg(feature = "exif")] pub mod exif; pub mod json; +#[cfg(feature = "avdict")] +pub mod video; diff --git a/src/data/video.rs b/src/data/video.rs new file mode 100644 index 0000000..07b46ef --- /dev/null +++ b/src/data/video.rs @@ -0,0 +1,26 @@ +use crate::avdict::AVDict; +use crate::avdict::AVDictError; +use crate::data::data::PixivData; +use crate::parser::description::parse_description; + +pub fn get_video_metadata(data: &PixivData) -> Result { + let mut d = AVDict::new(); + if data.title.is_some() { + let t = data.title.as_ref().unwrap(); + d.set("title", t, None)?; + } + if data.author.is_some() { + let au = data.author.as_ref().unwrap(); + d.set("artist", au, None)?; + } + if data.description.is_some() { + let odesc = data.description.as_ref().unwrap(); + let desc = parse_description(odesc); + let des = match &desc { + Some(d) => { d } + None => { odesc } + }; + d.set("comment", des, None)?; + } + Ok(d) +} diff --git a/src/download.rs b/src/download.rs index 81d2527..3f27eef 100644 --- a/src/download.rs +++ b/src/download.rs @@ -1,10 +1,16 @@ +#[cfg(feature = "avdict")] +use crate::avdict::AVDict; use crate::data::data::PixivData; #[cfg(feature = "exif")] use crate::data::exif::add_exifdata_to_image; use crate::data::json::JSONDataFile; +#[cfg(feature = "ugoira")] +use crate::data::video::get_video_metadata; use crate::gettext; use crate::pixiv_link::PixivID; use crate::pixiv_web::PixivWebClient; +#[cfg(feature = "ugoira")] +use crate::ugoira::{UgoiraFrames, convert_ugoira_to_mp4}; use crate::utils::ask_need_overwrite; use crate::utils::get_file_name_from_url; use crate::webclient::WebClient; @@ -86,6 +92,95 @@ impl Main { println!("{}", gettext("Failed to save metadata to JSON file.")); return 1; } + let illust_type = if ajax_ver { + (&re["illustType"]).as_i64() + } else { + (&re["illust"][format!("{}", id).as_str()]["illustType"]).as_i64() + }; + if illust_type.is_some() { + let illust_type = illust_type.unwrap(); + match illust_type { + 0 => { } + 2 => { + let ugoira_data = pw.get_ugoira(id); + if ugoira_data.is_none() { + println!("{}", gettext("Failed to get ugoira's data.")); + return 1; + } + let ugoira_data = ugoira_data.unwrap(); + let src = (&ugoira_data["originalSrc"]).as_str(); + if src.is_none() { + println!("{}", gettext("Can not find source link for ugoira.")); + return 1; + } + let src = src.unwrap(); + let file_name = get_file_name_from_url(src); + if file_name.is_none() { + println!("{} {}", gettext("Failed to get file name from url:"), src); + return 1; + } + let file_name = file_name.unwrap(); + let file_name = base.join(file_name); + let dw = if file_name.exists() { + match pw.helper.overwrite() { + Some(overwrite) => { overwrite } + None => { ask_need_overwrite(file_name.to_str().unwrap()) } + } + } else { + true + }; + if dw { + let r = pw.download_image(src); + if r.is_none() { + println!("{} {}", gettext("Failed to download ugoira:"), src); + return 1; + } + let r = r.unwrap(); + let re = WebClient::download_stream(&file_name, r, &pw.helper); + if re.is_err() { + println!("{} {}", gettext("Failed to download ugoira:"), src); + return 1; + } + println!( + "{} {} -> {}", + gettext("Downloaded ugoira:"), + src, + file_name.to_str().unwrap_or("(null)") + ); + } + #[cfg(feature = "ugoira")] + { + let metadata = match get_video_metadata(&datas) { + Ok(m) => { m } + Err(e) => { + println!("{} {}", gettext("Warning: Failed to generate video's metadata:"), e); + AVDict::new() + } + }; + let options = AVDict::new(); + match UgoiraFrames::from_json(&ugoira_data["frames"]) { + Ok(frames) => { + let output_file_name = base.join(format!("{}.mp4", id)); + let re = convert_ugoira_to_mp4(&file_name, &output_file_name, &frames, 60f32, &options, &metadata); + if re.is_err() { + println!("{} {}", gettext("Failed to convert from ugoira to mp4 video file:"), re.unwrap_err()); + return 1; + } + println!("{}", gettext("Converted -> ").replace("", file_name.to_str().unwrap_or("(null)")).replace("", output_file_name.to_str().unwrap_or("(null)")).as_str()); + } + Err(e) => { + println!("{} {}", gettext("Failed to parse frames:"), e); + return 1; + } + } + } + return 0; + } + _ => { println!("{} {}", gettext("Warning: Unknown illust type:"), illust_type) } + } + } else { + println!("{}", gettext("Warning: Failed to get illust's type.")); + } if pages_data.is_some() { #[cfg(feature = "exif")] let mut np = 0u16; @@ -137,7 +232,7 @@ impl Main { return 1; } let r = r.unwrap(); - let re = WebClient::download_stream(&file_name, r); + let re = WebClient::download_stream(&file_name, r, &pw.helper); if re.is_err() { println!("{} {}", gettext("Failed to download image:"), link); return 1; @@ -207,7 +302,7 @@ impl Main { return 1; } let r = r.unwrap(); - let re = WebClient::download_stream(&file_name, r); + let re = WebClient::download_stream(&file_name, r, &pw.helper); if re.is_err() { println!("{} {}", gettext("Failed to download image:"), link); return 1; diff --git a/src/ext/json.rs b/src/ext/json.rs new file mode 100644 index 0000000..875359e --- /dev/null +++ b/src/ext/json.rs @@ -0,0 +1,35 @@ +use json::JsonValue; + +pub trait ToJson { + fn to_json(&self) -> Option; +} + +impl ToJson for &str { + fn to_json(&self) -> Option { + Some(JsonValue::String(String::from(*self))) + } +} + +impl ToJson for &String { + fn to_json(&self) -> Option { + Some(JsonValue::String((*self).to_string())) + } +} + +impl ToJson for String { + fn to_json(&self) -> Option { + Some(JsonValue::String(self.to_string())) + } +} + +impl ToJson for JsonValue { + fn to_json(&self) -> Option { + Some(self.clone()) + } +} + +impl ToJson for &JsonValue { + fn to_json(&self) -> Option { + Some((*self).clone()) + } +} diff --git a/src/ext/mod.rs b/src/ext/mod.rs index 1255dca..6f9d062 100644 --- a/src/ext/mod.rs +++ b/src/ext/mod.rs @@ -1,5 +1,6 @@ pub mod cstr; #[cfg(feature = "flagset")] pub mod flagset; -#[cfg(any(feature = "exif", feature = "avdict"))] +pub mod json; +#[cfg(any(feature = "exif", feature = "avdict", feature = "ugoira"))] pub mod rawhandle; diff --git a/src/ext/rawhandle.rs b/src/ext/rawhandle.rs index ac5e670..41d3f67 100644 --- a/src/ext/rawhandle.rs +++ b/src/ext/rawhandle.rs @@ -2,4 +2,9 @@ pub trait ToRawHandle { /// Return raw pointer of the handle unsafe fn to_raw_handle(&self) -> *mut T; + + /// Return the const raw pointer of the handle + unsafe fn to_const_handle(&self) -> *const T { + self.to_raw_handle() as *const T + } } diff --git a/src/main.rs b/src/main.rs index a5e9e18..f737a2a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,4 @@ +extern crate atty; #[cfg(feature = "c_fixed_string")] extern crate c_fixed_string; extern crate chrono; @@ -7,6 +8,7 @@ extern crate derive_more; extern crate flagset; extern crate futures_util; extern crate json; +extern crate indicatif; #[cfg(feature = "int-enum")] extern crate int_enum; #[macro_use] @@ -22,11 +24,11 @@ extern crate utf16string; extern crate xml; #[cfg(feature = "avdict")] -#[doc(hidden)] mod _avdict; #[cfg(feature = "exif")] -#[doc(hidden)] mod _exif; +#[cfg(feature = "ugoira")] +mod _ugoira; mod author_name_filter; #[cfg(feature = "avdict")] mod avdict; @@ -49,6 +51,8 @@ mod retry_interval; mod settings; mod settings_list; mod stdext; +#[cfg(feature = "ugoira")] +mod ugoira; mod utils; mod webclient; diff --git a/src/opthelper.rs b/src/opthelper.rs index 4f7d588..4de4644 100644 --- a/src/opthelper.rs +++ b/src/opthelper.rs @@ -117,4 +117,9 @@ impl<'a> OptHelper<'a> { } false } + + /// Return whether to use progress bar. + pub fn use_progress_bar(&self) -> bool { + atty::is(atty::Stream::Stdout) + } } diff --git a/src/pixiv_link.rs b/src/pixiv_link.rs index 29fc61b..7f706ce 100644 --- a/src/pixiv_link.rs +++ b/src/pixiv_link.rs @@ -1,4 +1,4 @@ -use crate::data::json::ToJson; +use crate::ext::json::ToJson; use json::JsonValue; use regex::Regex; use reqwest::IntoUrl; diff --git a/src/pixiv_web.rs b/src/pixiv_web.rs index 2e6bdd6..d113181 100644 --- a/src/pixiv_web.rs +++ b/src/pixiv_web.rs @@ -214,6 +214,20 @@ impl<'a> PixivWebClient<'a> { v } + pub fn get_ugoira(&mut self, id: u64) -> Option { + self.auto_init(); + let r = self.client.get(format!("https://www.pixiv.net/ajax/illust/{}/ugoira_meta", id), None); + if r.is_none() { + return None; + } + let r = r.unwrap(); + let v = self.deal_json(r); + if self.helper.verbose() && v.is_some() { + println!("{} {}", gettext("Ugoira's data:"), v.as_ref().unwrap().pretty(2)); + } + v + } + pub fn logined(&self) -> bool { if self.data.is_none() { return false; diff --git a/src/retry_interval.rs b/src/retry_interval.rs index 6cc6e2e..e597f88 100644 --- a/src/retry_interval.rs +++ b/src/retry_interval.rs @@ -1,4 +1,4 @@ -use crate::data::json::ToJson; +use crate::ext::json::ToJson; use crate::dur::Dur; use crate::dur::DurType; use crate::gettext; diff --git a/src/ugoira.rs b/src/ugoira.rs new file mode 100644 index 0000000..cbd2b48 --- /dev/null +++ b/src/ugoira.rs @@ -0,0 +1,377 @@ +use crate::_ugoira; +use crate::avdict::AVDict; +use crate::avdict::AVDictCodeError; +use crate::ext::cstr::ToCStr; +use crate::ext::cstr::ToCStrError; +use crate::ext::json::ToJson; +use crate::ext::rawhandle::ToRawHandle; +use crate::gettext; +use crate::stdext::TryErr; +use std::convert::AsRef; +use std::default::Default; +use std::ffi::CStr; +use std::ffi::OsStr; +use std::fmt::Debug; +use std::fmt::Display; +use std::ops::Drop; +use std::os::raw::c_int; +use std::os::raw::c_void; +use std::str::Utf8Error; + +const UGOIRA_OK: c_int = _ugoira::UGOIRA_OK as c_int; +const UGOIRA_NULL_POINTER: c_int = _ugoira::UGOIRA_NULL_POINTER as c_int; +const UGOIRA_ZIP: c_int = _ugoira::UGOIRA_ZIP as c_int; +const UGOIRA_INVALID_MAX_FPS: c_int = _ugoira::UGOIRA_INVALID_MAX_FPS as c_int; +const UGOIRA_INVALID_FRAMES: c_int = _ugoira::UGOIRA_INVALID_FRAMES as c_int; +const UGOIRA_INVALID_CRF: c_int = _ugoira::UGOIRA_INVALID_CRF as c_int; +const UGOIRA_REMOVE_OUTPUT_FILE_FAILED: c_int = _ugoira::UGOIRA_REMOVE_OUTPUT_FILE_FAILED as c_int; +const UGOIRA_OOM: c_int = _ugoira::UGOIRA_OOM as c_int; +const UGOIRA_NO_VIDEO_STREAM: c_int = _ugoira::UGOIRA_NO_VIDEO_STREAM as c_int; +const UGOIRA_NO_AVAILABLE_DECODER: c_int = _ugoira::UGOIRA_NO_AVAILABLE_DECODER as c_int; +const UGOIRA_NO_AVAILABLE_ENCODER: c_int = _ugoira::UGOIRA_NO_AVAILABLE_ENCODER as c_int; +const UGOIRA_OPEN_FILE: c_int = _ugoira::UGOIRA_OPEN_FILE as c_int; +const UGOIRA_UNABLE_SCALE: c_int = _ugoira::UGOIRA_UNABLE_SCALE as c_int; + +#[derive(Debug, derive_more::From, PartialEq)] +pub enum UgoiraError { + String(String), + Utf8(Utf8Error), + ToCStr(ToCStrError), + FfmpegError(AVDictCodeError), + CodeError(UgoiraCodeError), + ZipError(UgoiraZipError), + ZipError2(UgoiraZipError2), +} + +impl Display for UgoiraError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::String(s) => { f.write_str(s) } + Self::Utf8(s) => { f.write_fmt(format_args!("{} {}", gettext("Failed to decode string with UTF-8:"), s)) } + Self::ToCStr(s) => { f.write_fmt(format_args!("{}", s)) } + Self::FfmpegError(s) => { f.write_fmt(format_args!("{}", s)) } + Self::CodeError(s) => { f.write_fmt(format_args!("{}", s)) } + Self::ZipError(s) => { f.write_fmt(format_args!("{}", s)) } + Self::ZipError2(s) => { f.write_fmt(format_args!("{}", s)) } + } + } +} + +impl From<&str> for UgoiraError { + fn from(s: &str) -> Self { + Self::String(String::from(s)) + } +} + +impl From for UgoiraError { + fn from(v: c_int) -> Self { + if v < 0 { + Self::FfmpegError(AVDictCodeError::from(v)) + } else { + Self::CodeError(UgoiraCodeError::from(v)) + } + } +} + +impl From<_ugoira::UgoiraError> for UgoiraError { + fn from(v: _ugoira::UgoiraError) -> Self { + if v.code < 0 { + Self::FfmpegError(AVDictCodeError::from(v.code)) + } else if v.code == UGOIRA_ZIP { + if v.zip_err != 0 { + Self::ZipError(UgoiraZipError::from(v.zip_err)) + } else { + Self::ZipError2(UgoiraZipError2::from(v.zip_err2)) + } + } else { + Self::CodeError(UgoiraCodeError::from(v.code)) + } + } +} + +#[derive(Clone, Copy, PartialEq, PartialOrd)] +pub struct UgoiraCodeError { + code: c_int, +} + +impl UgoiraCodeError { + fn to_str(&self) -> &'static str { + match self.code { + UGOIRA_OK => { "OK" } + UGOIRA_NULL_POINTER => { gettext("Arguments have null pointers.") } + UGOIRA_INVALID_MAX_FPS => { gettext("Invalid max fps.") } + UGOIRA_INVALID_FRAMES => { gettext("Invalid frames.") } + UGOIRA_INVALID_CRF => { gettext("Invalid crf.") } + UGOIRA_REMOVE_OUTPUT_FILE_FAILED => { gettext("Can not remove output file.") } + UGOIRA_OOM => { gettext("Out of memory.") } + UGOIRA_NO_VIDEO_STREAM => { gettext("No video stream available in the file.") } + UGOIRA_NO_AVAILABLE_DECODER => { gettext("No available decoder.") } + UGOIRA_NO_AVAILABLE_ENCODER => { gettext("No available encoder.") } + UGOIRA_OPEN_FILE => { gettext("Failed to open output file.") } + UGOIRA_UNABLE_SCALE => { gettext("Unable to scale image.") } + _ => { gettext("Unknown error.") } + } + } +} + +impl Debug for UgoiraCodeError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(format!("{}({})", self.to_str(), self.code).as_str()) + } +} + +impl Display for UgoiraCodeError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(self.to_str()) + } +} + +impl From for UgoiraCodeError { + fn from(v: c_int) -> Self { + Self { + code: v, + } + } +} + +#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)] +pub struct UgoiraZipError { + code: c_int, +} + +impl UgoiraZipError { + pub fn to_str(&self) -> Result { + let s = unsafe { _ugoira::ugoira_get_zip_err_msg(self.code) }; + if s.is_null() { + Err(gettext("Out of memory."))?; + } + let ss = unsafe { CStr::from_ptr(s) }; + let ss = ss.to_owned(); + unsafe { _ugoira::ugoira_mfree(s as *mut c_void) }; + let re = ss.to_str()?; + Ok(String::from(re)) + } +} + +impl Display for UgoiraZipError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(self.to_str().unwrap_or(format!("{} {}", gettext("Failed to get error message:"), self.code)).as_str()) + } +} + +impl From for UgoiraZipError { + fn from(v: c_int) -> Self { + Self { + code: v, + } + } +} + +pub struct UgoiraZipError2 { + err: *mut _ugoira::zip_error_t, +} + +impl UgoiraZipError2 { + pub fn to_str(&self) -> Result { + if self.err.is_null() { + return Ok(String::from(gettext("No error."))); + } + let s = unsafe { _ugoira::ugoira_get_zip_err2_msg(self.err) }; + if s.is_null() { + Err(gettext("Out of memory."))?; + } + let ss = unsafe { CStr::from_ptr(s) }; + let ss = ss.to_owned(); + unsafe { _ugoira::ugoira_mfree(s as *mut c_void) }; + let re = ss.to_str()?; + Ok(String::from(re)) + } +} + +impl Debug for UgoiraZipError2 { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if self.err.is_null() { + f.write_str("UgoiraError2 { None }") + } else { + let err = unsafe { *self.err }; + f.write_fmt(format_args!("UgoiraZipError2 {{ {}, {} }}", err.sys_err, err.zip_err )) + } + } +} + +impl Display for UgoiraZipError2 { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(self.to_str().unwrap_or(format!("{} {}, {}", gettext("Failed to get error message:"), unsafe { (*self.err).sys_err }, unsafe { (*self.err).zip_err })).as_str()) + } +} + +impl Drop for UgoiraZipError2 { + fn drop(&mut self) { + if !self.err.is_null() { + unsafe { _ugoira::free_ugoira_error(self.err) }; + self.err = 0 as *mut _ugoira::zip_error_t; + } + } +} + +impl From<*mut _ugoira::zip_error_t> for UgoiraZipError2 { + fn from(err: *mut _ugoira::zip_error_t) -> Self { + Self { + err, + } + } +} + +impl PartialEq for UgoiraZipError2 { + fn eq(&self, other: &Self) -> bool { + if self.err.is_null() && other.err.is_null() { + true + } else if self.err.is_null() || other.err.is_null() { + false + } else { + let e = unsafe { *self.err }; + let e2 = unsafe { *other.err }; + if e.sys_err == e2.sys_err && e.zip_err == e2.zip_err { + true + } else { + false + } + } + } +} + +impl ToRawHandle<_ugoira::zip_error_t> for UgoiraZipError2 { + unsafe fn to_raw_handle(&self) -> *mut _ugoira::zip_error_t { + self.err + } +} + +pub struct UgoiraFrames { + head: *mut _ugoira::UgoiraFrame, + tail: *mut _ugoira::UgoiraFrame, +} + +#[allow(dead_code)] +impl UgoiraFrames { + pub fn new() -> Self { + Self { + head: 0 as *mut _ugoira::UgoiraFrame, + tail: 0 as *mut _ugoira::UgoiraFrame, + } + } + + pub fn append(&mut self, file: T, delay: f32) -> Result<(), UgoiraError> { + let f = file.to_cstr()?; + if delay <= 0f32 { + Err(gettext("Delay should be greater than 0."))?; + } + let re = unsafe { _ugoira::new_ugoira_frame(f.as_ptr(), delay, self.tail) }; + if re.is_null() { + Err(gettext("Out of memory."))?; + } + if self.head.is_null() { + self.head = re; + } + self.tail = re; + Ok(()) + } + + pub fn from_json(value: T) -> Result { + let obj = value.to_json().try_err(gettext("Failed to get JSON object."))?; + if !obj.is_array() { + Err(gettext("Unsupported JSON type."))?; + } + let mut r = Self::new(); + for o in obj.members() { + if !o.is_object() { + Err(gettext("Unsupported JSON type."))?; + } + let file = o["file"].as_str().try_err(gettext("File is needed."))?; + let delay = o["delay"].as_f32().try_err(gettext("Delay is needed."))?; + r.append(file, delay)?; + } + Ok(r) + } + + pub fn len(&self) -> usize { + if self.head.is_null() { + return 0; + } + let mut c = 1; + let mut now = self.head; + while unsafe { !(*now).next.is_null() } { + now = unsafe { (*now).next }; + c += 1; + } + return c; + } +} + +impl AsRef for UgoiraFrames { + fn as_ref(&self) -> &Self { + self + } +} + +impl Default for UgoiraFrames { + fn default() -> Self { + Self::new() + } +} + +impl Drop for UgoiraFrames { + fn drop(&mut self) { + if !self.head.is_null() { + unsafe { _ugoira::free_ugoira_frames(self.head) }; + self.head = 0 as *mut _ugoira::UgoiraFrame; + } + self.tail = 0 as *mut _ugoira::UgoiraFrame; + } +} + +impl ToRawHandle<_ugoira::UgoiraFrame> for UgoiraFrames { + unsafe fn to_raw_handle(&self) -> *mut _ugoira::UgoiraFrame { + self.head + } +} + +impl ToRawHandle<_ugoira::AVDictionary> for AVDict { + unsafe fn to_raw_handle(&self) -> *mut _ugoira::AVDictionary { + self.m as *mut _ugoira::AVDictionary + } +} + +pub fn convert_ugoira_to_mp4 + ?Sized, D: AsRef + ?Sized, F: AsRef + ?Sized, O: AsRef + ?Sized, M: AsRef + ?Sized>(src: &S, dest: &D, frames: &F, max_fps: f32, opts: &O, metadata: &M) -> Result<(), UgoiraError> { + let src = src.as_ref(); + let dest = dest.as_ref(); + let frames = frames.as_ref(); + let opts = opts.as_ref(); + let metadata = metadata.as_ref(); + let src = src.to_str().try_err(gettext("Failed to convert path."))?.to_cstr()?; + let dest = dest.to_str().try_err(gettext("Failed to convert path."))?.to_cstr()?; + let re = unsafe { _ugoira::convert_ugoira_to_mp4(src.as_ptr(), dest.as_ptr(), frames.to_const_handle(), max_fps, opts.to_const_handle(), metadata.to_const_handle()) }; + if re.code != 0 { + Err(re)?; + } + Ok(()) +} + +#[test] +fn test_ugoira_frames() { + let mut f = UgoiraFrames::new(); + assert_eq!(0, f.len()); + f.append("test.png", 32f32).unwrap(); + assert_eq!(1, f.len()); + f.append("test2.png", 31f32).unwrap(); + assert_eq!(2, f.len()); + f.append("fgng", 23f32).unwrap(); + assert_eq!(3, f.len()); + let f2 = UgoiraFrames::from_json(json::array![{"file": "a.jpg", "delay": 2}]).unwrap(); + assert_eq!(1, f2.len()); +} + +#[test] +fn test_ugoira_zip_error() { + let e = UgoiraZipError::from(3); + assert_eq!(Ok(String::from("Closing zip archive failed: No error")), e.to_str()); +} diff --git a/src/webclient.rs b/src/webclient.rs index 2b4677b..3065c17 100644 --- a/src/webclient.rs +++ b/src/webclient.rs @@ -4,7 +4,10 @@ use crate::cookies::Cookie; use crate::cookies::CookieJar; use crate::gettext; use crate::list::NonTailList; +use crate::opthelper::OptHelper; use futures_util::StreamExt; +use indicatif::ProgressBar; +use indicatif::ProgressStyle; use json::JsonValue; use reqwest::{Client, IntoUrl, RequestBuilder, Response}; use spin_on::spin_on; @@ -275,7 +278,23 @@ impl WebClient { /// * `file_name` - File name /// * `r` - Response /// Note: If file already exists, will remove existing file first. - pub fn download_stream + ?Sized>(file_name: &S, r: Response) -> Result<(), ()> { + pub fn download_stream + ?Sized>(file_name: &S, r: Response, opt: &OptHelper) -> Result<(), ()> { + let content_length = r.content_length(); + let use_progress_bar = match &content_length { + Some(_) => { opt.use_progress_bar() } + None => { false } + }; + let mut bar = if use_progress_bar { + Some(ProgressBar::new(content_length.unwrap())) + } else { + None + }; + if bar.is_some() { + bar.as_mut().unwrap().set_style(ProgressStyle::default_bar() + .template("[{elapsed_precise}] [{wide_bar:.green/yellow}] {bytes}/{total_bytes} ({bytes_per_sec}, {eta}) {msg:40}") + .progress_chars("#>-")); + } + let mut downloaded = 0usize; let p = Path::new(file_name); if p.exists() { let re = remove_file(p); @@ -284,6 +303,10 @@ impl WebClient { return Err(()); } } + if bar.is_some() { + let tmp = p.file_name().unwrap_or(p.as_os_str()); + bar.as_mut().unwrap().set_message(gettext("Downloading \"\".").replace("", tmp.to_str().unwrap_or(""))); + } let f = File::create(p); if f.is_err() { println!("{} {}", gettext("Failed to create file:"), f.unwrap_err()); @@ -297,6 +320,10 @@ impl WebClient { return Err(()); } let data = data.unwrap(); + downloaded += data.len(); + if bar.is_some() { + bar.as_mut().unwrap().set_position(downloaded as u64); + } let r = f.write(&data); if r.is_err() { println!("{} {}", gettext("Failed to write file:"), r.unwrap_err()); diff --git a/ugoira/CMakeLists.txt b/ugoira/CMakeLists.txt new file mode 100644 index 0000000..d7586eb --- /dev/null +++ b/ugoira/CMakeLists.txt @@ -0,0 +1,52 @@ +cmake_minimum_required(VERSION 3.9) + +project(ugoira) + +if (MSVC) + add_compile_options(/utf-8) +endif() + +include(GNUInstallDirs) + +list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/../cmake") +include(GetLinkLibraries) +find_package(AVUTIL REQUIRED) +find_package(AVFORMAT REQUIRED) +find_package(AVCODEC REQUIRED) +find_package(SWSCALE REQUIRED) +find_package(LIBZIP REQUIRED) + +option(UTILS_LIBRARY "The path of utils of library." "") +if (UTILS_LIBRARY) + set(UTILS_TARGET "${UTILS_LIBRARY}") +else() + set(ENABLE_ICONV OFF CACHE BOOL "Libiconv is not needed.") + add_subdirectory("${CMAKE_CURRENT_SOURCE_DIR}/../utils" "${CMAKE_BINARY_DIR}/utils") + set(UTILS_TARGET utils) +endif() + +include(CheckSymbolExists) +if (WIN32) + check_symbol_exists(sscanf_s "stdio.h" HAVE_SSCANF_S) +endif() + +configure_file("${CMAKE_CURRENT_SOURCE_DIR}/ugoira_config.h.in" "${CMAKE_CURRENT_BINARY_DIR}/ugoira_config.h") + +set(UGOIRA_FILES +ugoira.h +src/ugoira.c +"${CMAKE_CURRENT_BINARY_DIR}/ugoira_config.h" +) + +include_directories("${CMAKE_CURRENT_SOURCE_DIR}/../utils") +include_directories("${CMAKE_CURRENT_BINARY_DIR}") + +add_library(ugoira STATIC "${UGOIRA_FILES}") +target_link_libraries(ugoira AVUTIL::AVUTIL AVFORMAT::AVFORMAT AVCODEC::AVCODEC SWSCALE::SWSCALE LIBZIP::LIBZIP ${UTILS_TARGET}) +target_compile_definitions(ugoira PRIVATE BUILD_UGOIRA) + +get_link_libraries(OUT ugoira) +file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/ugoira_dep.txt" "${OUT}") + +install(TARGETS ugoira) +install(FILES "${CMAKE_CURRENT_BINARY_DIR}/ugoira_dep.txt" DESTINATION ${CMAKE_INSTALL_PREFIX}) diff --git a/ugoira/src/ugoira.c b/ugoira/src/ugoira.c new file mode 100644 index 0000000..a184471 --- /dev/null +++ b/ugoira/src/ugoira.c @@ -0,0 +1,506 @@ +#include "ugoira_config.h" +#include "../ugoira.h" + +#include +#include +#include "cfileop.h" +#include "cmath.h" +#include "cstr_util.h" +#include "libavutil/avutil.h" +#include "libavutil/opt.h" +#include "libavformat/avformat.h" +#include "libavcodec/avcodec.h" +#include "libswscale/swscale.h" +#include "zip.h" + +#define RERR(e) (UgoiraError){ e, 0, 0 } +#define RZERR(e) (UgoiraError){ UGOIRA_ZIP, e, 0 } +#define CHECK_ZIP_ERR(zip, e) { zip_error_t* tmp = zip_get_error(zip); \ + if (tmp->sys_err || tmp->zip_err) { \ + copy_zip_error(&e, tmp); \ + goto end; \ + } \ +} + +#if HAVE_SSCANF_S +#define sscanf sscanf_s +#endif + +UgoiraFrame* new_ugoira_frame(const char* file, float delay, UgoiraFrame* prev) { + if (!file || delay <= 0) return NULL; + UgoiraFrame* f = malloc(sizeof(UgoiraFrame)); + if (!f) return NULL; + memset(f, 0, sizeof(UgoiraFrame)); + if (cstr_util_copy_str(&f->file, file)) { + free(f); + return NULL; + } + f->delay = delay; + if (prev) prev->next = f; + return f; +} + +void free_ugoira_frame(UgoiraFrame* f) { + if (!f) return; + if (f->file) free(f->file); + free(f); +} + +void free_ugoira_frames(UgoiraFrame* frames) { + if (!frames) return; + UgoiraFrame* next = frames->next; + free_ugoira_frame(frames); + while (next) { + frames = next; + next = frames->next; + free_ugoira_frame(frames); + } +} + +float ugoira_cal_fps(const UgoiraFrame* frames, float max_fps) { + if (!frames) return max_fps; + int re = frames->delay; + const UgoiraFrame* cur = frames->next; + while (cur) { + re = GCD(re, cur->delay); + cur = cur->next; + } + return FFMIN(1000 / ((float)re), max_fps); +} + +int ugoira_is_supported_pixfmt(enum AVPixelFormat fmt, const enum AVPixelFormat* fmts) { + size_t i = 0; + if (fmt == AV_PIX_FMT_NONE || !fmts) return 0; + while (fmts[i] != AV_PIX_FMT_NONE) { + if (fmt == fmts[i]) return 1; + i++; + } + return 0; +} + +const AVCodec* ugoira_find_encoder() { + const AVCodec* c = avcodec_find_encoder_by_name("libx264"); + if (!c) c = avcodec_find_encoder(AV_CODEC_ID_H264); + return c; +} + +int check_ugoira_frames(const UgoiraFrame* frames) { + if (!frames) return 0; + const UgoiraFrame* cur = frames; + while (cur) { + if (!cur->file || cur->delay <= 0) return 0; + cur = cur->next; + } + return 1; +} + +void copy_zip_error(zip_error_t* dest, zip_error_t* src) { + dest->sys_err = src->sys_err; + dest->zip_err = src->zip_err; + if (dest->str) { + free(dest->str); + dest->str = NULL; + } + cstr_util_copy_str(&dest->str, src->str); +} + +int zip_file_read_packet(void* f, uint8_t* buf, int buf_size) { + zip_file_t* zf = f; + int64_t count = zip_fread(zf, (void*)buf, buf_size); + if (count == -1) { + return AVERROR(EINVAL); + } + if (count == 0) { + return AVERROR_EOF; + } + return count; +} + +int ugoira_encode_video(AVFrame* ofr, AVFormatContext* oc, AVCodecContext* eoc, char* writed_data, int64_t* pts, unsigned int stream_index, AVRational time_base) { + if (!oc || !eoc || !writed_data) return UGOIRA_NULL_POINTER; + if (ofr && !pts) return UGOIRA_NULL_POINTER; + int err = UGOIRA_OK; + *writed_data = 0; + AVPacket* pkt = av_packet_alloc(); + if (!pkt) { + return UGOIRA_OOM; + } + if (ofr) { + ofr->pts = *pts; + *pts += av_rescale_q_rnd(1, time_base, oc->streams[stream_index]->time_base, AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX); + ofr->pkt_dts = ofr->pts; + } + if ((err = avcodec_send_frame(eoc, ofr)) < 0) { + if (err == AVERROR_EOF) { + err = 0; + } else { + goto end; + } + } + err = avcodec_receive_packet(eoc, pkt); + if (err >= 0) { + err = 0; + *writed_data = 1; + } else if (err == AVERROR_EOF || err == AVERROR(EAGAIN)) { + err = 0; + goto end; + } else { + goto end; + } + if (*writed_data && pkt) { + pkt->stream_index = stream_index; + if ((err = av_write_frame(oc, pkt)) < 0) { + goto end; + } + err = 0; + } +end: + if (pkt) av_packet_free(&pkt); + return err; +} + +UgoiraError convert_ugoira_to_mp4(const char* src, const char* dest, const UgoiraFrame* frames, float max_fps, const AVDictionary* opts, const AVDictionary* metadata) { + if (!src || !dest || !frames) return RERR(UGOIRA_NULL_POINTER); + int err = UGOIRA_OK; + int zip_err = 0; + zip_t* zip = NULL; + int dcrf = 18; + AVDictionaryEntry* tmp_ent = NULL; + if (max_fps <= 0) { + return RERR(UGOIRA_INVALID_MAX_FPS); + } + if (!check_ugoira_frames(frames)) { + return RERR(UGOIRA_INVALID_FRAMES); + } + tmp_ent = av_dict_get(opts, "crf", NULL, 0); + if (tmp_ent) { + int tmp = 0; + if (sscanf(tmp_ent->value, "%i", &tmp) != 1) { + return RERR(UGOIRA_INVALID_CRF); + } + dcrf = tmp; + } + zip_error_t ziperr; + AVRational fps = { (int)(ugoira_cal_fps(frames, max_fps) * AV_TIME_BASE + 0.5), AV_TIME_BASE }; + AVRational time_base = { fps.den, fps.num }; + AVFormatContext* ic = NULL, * oc = NULL; + AVIOContext* iioc = NULL; + AVFrame* ifr = NULL, * ofr = NULL; + AVCodecContext* eoc = NULL, * eic = NULL; + AVStream* is = NULL, * os = NULL; + const AVCodec* output_codec = NULL, * input_codec = NULL; + const UgoiraFrame* cur_frame = frames; + size_t i = 0; + struct SwsContext* sws_ctx = NULL; + enum AVPixelFormat pre_pixfmt = AV_PIX_FMT_NONE; + int pre_width = -1, pre_height = -1; + zip_file_t* zf = NULL; + unsigned char* buff = NULL; + AVPacket pkt; + int64_t pts = 0, max_de = 0; + const static AVRational tb = { 1, 1000 }; + char writed = 0; + zip_error_init(&ziperr); + if (fileop_exists(dest)) { + if (!fileop_remove(dest)) { + return RERR(UGOIRA_REMOVE_OUTPUT_FILE_FAILED); + } + } + if (!(ifr = av_frame_alloc())) { + err = UGOIRA_OOM; + goto end; + } + if (!(ofr = av_frame_alloc())) { + err = UGOIRA_OOM; + goto end; + } + zip = zip_open(src, ZIP_RDONLY, &zip_err); + if (!zip) { + goto end; + } + while (cur_frame) { + if (i != 0) { + if (eic) { + avcodec_free_context(&eic); + eic = NULL; + } + avformat_close_input(&ic); + ic = NULL; + if (zip_fclose(zf)) { + CHECK_ZIP_ERR(zip, ziperr); + } + zf = NULL; + if (iioc) av_freep(&iioc->buffer); + avio_context_free(&iioc); + iioc = NULL; + } + if (!(ic = avformat_alloc_context())) { + err = UGOIRA_OOM; + goto end; + } + zf = zip_fopen(zip, cur_frame->file, 0); + if (!zf) { + CHECK_ZIP_ERR(zip, ziperr); + } + buff = av_malloc(4096); + if (!buff) { + err = UGOIRA_OOM; + goto end; + } + if (!(iioc = avio_alloc_context(buff, 4096, 0, (void*)zf, zip_file_read_packet, NULL, NULL))) { + err = UGOIRA_OOM; + goto end; + } + ic->pb = iioc; + if ((err = avformat_open_input(&ic, NULL, NULL, NULL)) < 0) { + goto end; + } + if ((err = avformat_find_stream_info(ic, NULL)) < 0) { + goto end; + } + err = 0; + for (unsigned int si = 0; si < ic->nb_streams; i++) { + AVStream* s = ic->streams[si]; + if (s->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) { + is = s; + break; + } + } + if (!is) { + err = UGOIRA_NO_VIDEO_STREAM; + goto end; + } + if (!(input_codec = avcodec_find_decoder(is->codecpar->codec_id))) { + err = UGOIRA_NO_AVAILABLE_DECODER; + goto end; + } + if (!(eic = avcodec_alloc_context3(input_codec))) { + err = UGOIRA_OOM; + goto end; + } + if ((err = avcodec_parameters_to_context(eic, is->codecpar)) < 0) { + goto end; + } + err = 0; + if ((err = avcodec_open2(eic, input_codec, NULL)) < 0) { + goto end; + } + if (i == 0) { + AVDictionaryEntry* force_yuv420p = NULL; + output_codec = ugoira_find_encoder(); + if (!output_codec) { + err = UGOIRA_NO_AVAILABLE_ENCODER; + goto end; + } + if ((err = avformat_alloc_output_context2(&oc, NULL, "mp4", dest)) < 0) { + goto end; + } + if (metadata) { + if ((err = av_dict_copy(&oc->metadata, metadata, 0)) < 0) { + goto end; + } + } + err = 0; + if (!(eoc = avcodec_alloc_context3(output_codec))) { + err = UGOIRA_OOM; + goto end; + } + eoc->width = eic->width; + eoc->height = eic->height; + eoc->sample_aspect_ratio = eic->sample_aspect_ratio; + eoc->framerate = fps; + if (opts) { + force_yuv420p = av_dict_get(opts, "force_yuv420p", NULL, 0); + } + if (!force_yuv420p && ugoira_is_supported_pixfmt(eic->pix_fmt, output_codec->pix_fmts)) { + eoc->pix_fmt = eic->pix_fmt; + } else { + eoc->pix_fmt = AV_PIX_FMT_YUV420P; + } + ofr->width = eoc->width; + ofr->height = eoc->height; + ofr->format = eoc->pix_fmt; + eoc->time_base = AV_TIME_BASE_Q; + if (!strcmp(output_codec->name, "libx264")) { + AVDictionaryEntry* tmp = NULL; + if (opts) { + tmp = av_dict_get(opts, "preset", NULL, 0); + } + if (tmp) { + av_opt_set(eoc->priv_data, "preset", tmp->value, 0); + } else { + av_opt_set(eoc->priv_data, "preset", "slow", 0); + } + av_opt_set_int(eoc->priv_data, "crf", dcrf, 0); + if (opts) { + tmp = av_dict_get(opts, "level", NULL, 0); + } + if (tmp) { + av_opt_set(eoc->priv_data, "level", tmp->value, 0); + } + if (opts) { + tmp = av_dict_get(opts, "profile", NULL, 0); + } + if (tmp) { + av_opt_set(eoc->priv_data, "profile", tmp->value, 0); + } + } + if ((err = av_frame_get_buffer(ofr, 0)) < 0) { + goto end; + } + if (!(os = avformat_new_stream(oc, output_codec))) { + err = UGOIRA_OOM; + goto end; + } + os->avg_frame_rate = fps; + os->r_frame_rate = fps; + os->time_base = AV_TIME_BASE_Q; + if ((err = avcodec_open2(eoc, output_codec, NULL)) < 0) { + goto end; + } + if ((err = avcodec_parameters_from_context(os->codecpar, eoc)) < 0) { + goto end; + } + err = 0; + if (!(oc->oformat->flags & AVFMT_NOFILE)) { + int ret = avio_open(&oc->pb, dest, AVIO_FLAG_WRITE); + if (ret < 0) { + err = UGOIRA_OPEN_FILE; + goto end; + } + } + if ((err = avformat_write_header(oc, NULL)) < 0) { + goto end; + } + err = 0; + } + if (!sws_ctx || eic->pix_fmt != pre_pixfmt || eic->width != pre_width || eic->height != pre_height) { + if (sws_ctx) { + sws_freeContext(sws_ctx); + sws_ctx = NULL; + } + if (!(sws_ctx = sws_getContext(eic->width, eic->height, eic->pix_fmt, eoc->width, eoc->height, eoc->pix_fmt, SWS_BILINEAR, NULL, NULL, NULL))) { + err = UGOIRA_UNABLE_SCALE; + goto end; + } + pre_pixfmt = eic->pix_fmt; + pre_width = eic->width; + pre_height = eic->height; + } + while (1) { + if ((err = av_read_frame(ic, &pkt)) < 0) { + goto end; + } + if (pkt.stream_index != is->index) { + av_packet_unref(&pkt); + continue; + } + if ((err = avcodec_send_packet(eic, &pkt)) < 0) { + av_packet_unref(&pkt); + goto end; + } + av_packet_unref(&pkt); + err = avcodec_receive_frame(eic, ifr); + if (err >= 0) { + err = 0; + if ((err = av_frame_make_writable(ofr)) < 0) { + goto end; + } + if ((err = sws_scale(sws_ctx, (const uint8_t* const*)ifr->data, ifr->linesize, 0, ifr->height, ofr->data, ofr->linesize)) < 0) { + goto end; + } + err = 0; + } else if (err == AVERROR(EAGAIN)) { + err = 0; + continue; + } else { + goto end; + } + max_de += av_rescale_q_rnd(cur_frame->delay, tb, os->time_base, AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX); + while (pts < max_de) { + if ((err = ugoira_encode_video(ofr, oc, eoc, &writed, &pts, os->index, time_base)) != UGOIRA_OK) { + goto end; + } + } + break; + } + cur_frame = cur_frame->next; + i += 1; + } + if (os) { + while (1) { + if ((err = ugoira_encode_video(NULL, oc, eoc, &writed, NULL, os->index, time_base)) != UGOIRA_OK) { + goto end; + } + if (!writed) { + break; + } + } + } + if ((err = av_write_trailer(oc)) < 0) { + goto end; + } +end: + if (ifr) av_frame_free(&ifr); + if (ofr) av_frame_free(&ofr); + if (sws_ctx) sws_freeContext(sws_ctx); + if (eic) avcodec_free_context(&eic); + if (eoc) avcodec_free_context(&eoc); + if (oc) { + if (!(oc->oformat->flags & AVFMT_NOFILE)) avio_closep(&oc->pb); + avformat_free_context(oc); + } + if (ic) avformat_close_input(&ic); + if (iioc) { + av_freep(&iioc->buffer); + avio_context_free(&iioc); + } + if (zf) { + zip_err = zip_fclose(zf); + } + if (zip) { + zip_err = zip_close(zip); + } + if (ziperr.sys_err || ziperr.zip_err) { + zip_error_t* tmp = malloc(sizeof(zip_error_t)); + if (!tmp) { + return RERR(UGOIRA_OOM); + } + memcpy(tmp, &ziperr, sizeof(zip_error_t)); + return (UgoiraError){ UGOIRA_ZIP, 0, tmp }; + } + return zip_err ? RZERR(zip_err) : RERR(err); +} + +char* ugoira_get_zip_err_msg(int code) { + zip_error_t err; + zip_error_init_with_code(&err, code); + const char* tmp = zip_error_strerror(&err); + char* re = NULL; + if (cstr_util_copy_str(&re, tmp)) { + zip_error_fini(&err); + return NULL; + } + zip_error_fini(&err); + return re; +} + +void ugoira_mfree(void* data) { + if (data) free(data); +} + +void free_ugoira_error(zip_error_t* zip_err) { + if (!zip_err) return; + zip_error_fini(zip_err); + free(zip_err); +} + +char* ugoira_get_zip_err2_msg(zip_error_t* zip_err2) { + if (!zip_err2) return NULL; + const char* errmsg = zip_error_strerror(zip_err2); + char* re = NULL; + if (cstr_util_copy_str(&re, errmsg)) { + return NULL; + } + return re; +} diff --git a/ugoira/ugoira.h b/ugoira/ugoira.h new file mode 100644 index 0000000..f99d10a --- /dev/null +++ b/ugoira/ugoira.h @@ -0,0 +1,46 @@ +#ifndef _UGOIRA_H +#define _UGOIRA_H +#define UGOIRA_OK 0 +#define UGOIRA_NULL_POINTER 1 +#define UGOIRA_ZIP 2 +#define UGOIRA_INVALID_MAX_FPS 3 +#define UGOIRA_INVALID_FRAMES 4 +#define UGOIRA_INVALID_CRF 5 +#define UGOIRA_REMOVE_OUTPUT_FILE_FAILED 6 +#define UGOIRA_OOM 7 +#define UGOIRA_NO_VIDEO_STREAM 8 +#define UGOIRA_NO_AVAILABLE_DECODER 9 +#define UGOIRA_NO_AVAILABLE_ENCODER 10 +#define UGOIRA_OPEN_FILE 11 +#define UGOIRA_UNABLE_SCALE 12 +typedef struct UgoiraFrame { + char* file; + float delay; + struct UgoiraFrame* next; +} UgoiraFrame; +///
+typedef struct AVDictionary AVDictionary; +#ifndef BUILD_UGOIRA +typedef struct zip_error { + int zip_err; + int sys_err; + char* str; +} zip_error; +#else +typedef struct zip_error zip_error; +#endif +typedef struct zip_error zip_error_t; +typedef struct UgoiraError { + int code; + int zip_err; + zip_error_t* zip_err2; +} UgoiraError; +UgoiraFrame* new_ugoira_frame(const char* file, float delay, UgoiraFrame* prev); +void free_ugoira_frame(UgoiraFrame* f); +void free_ugoira_frames(UgoiraFrame* frames); +UgoiraError convert_ugoira_to_mp4(const char* src, const char* dest, const UgoiraFrame* frames, float max_fps, const AVDictionary* opts, const AVDictionary* metadata); +char* ugoira_get_zip_err_msg(int code); +void ugoira_mfree(void* data); +void free_ugoira_error(zip_error_t* zip_err); +char* ugoira_get_zip_err2_msg(zip_error_t* zip_err2); +#endif diff --git a/ugoira/ugoira_config.h.in b/ugoira/ugoira_config.h.in new file mode 100644 index 0000000..d13e691 --- /dev/null +++ b/ugoira/ugoira_config.h.in @@ -0,0 +1,4 @@ +#ifndef _UGOIRA_CONFIG_H +#define _UGOIRA_CONFIG_H +#cmakedefine HAVE_SSCANF_S @HAVE_SSCANF_S@ +#endif