├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── bash_completion └── subberthehut.c /.gitignore: -------------------------------------------------------------------------------- 1 | .clang_complete 2 | subberthehut 3 | *.swp 4 | *.o 5 | .vimsession 6 | *.srt 7 | *.mkv 8 | *.mp4 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | , 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | VERSION = 20 2 | 3 | PREFIX ?= /usr/local 4 | 5 | bash_completion_dir = $(shell pkg-config --silence-errors --variable=completionsdir bash-completion) 6 | 7 | override CFLAGS := -std=gnu99 -Wall -Wextra -pedantic -O2 -D_FORTIFY_SOURCE=2 \ 8 | $(shell xmlrpc-c-config client --cflags) \ 9 | $(shell pkg-config --cflags glib-2.0 zlib) \ 10 | -DVERSION=\"$(VERSION)\" \ 11 | $(CFLAGS) 12 | 13 | LDLIBS = $(shell xmlrpc-c-config client --libs) \ 14 | $(shell pkg-config --libs glib-2.0 zlib) \ 15 | $(LDFLAGS) 16 | 17 | subberthehut: subberthehut.o 18 | 19 | install: subberthehut check-bash-completion 20 | install -pDm755 subberthehut $(DESTDIR)$(PREFIX)/bin/subberthehut 21 | install -pDm644 bash_completion $(DESTDIR)$(bash_completion_dir)/subberthehut 22 | 23 | uninstall: check-bash-completion 24 | $(RM) $(DESTDIR)$(PREFIX)/bin/subberthehut 25 | $(RM) $(DESTDIR)$(bash_completion_dir)/subberthehut 26 | 27 | clean: 28 | $(RM) subberthehut subberthehut.o 29 | 30 | check-bash-completion: 31 | ifeq ($(bash_completion_dir),) 32 | $(error bash-completion directory not found, please install bash-completion) 33 | endif 34 | 35 | 36 | .PHONY: install uninstall clean check-bash-completion 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Obsolete 2 | 3 | The XML-RPC API used by subberthehut will be turned off at the end of 2023, see [this announcement](https://forum.opensubtitles.org/viewtopic.php?f=11&t=17930). I do not plan to migrate to the new API, therefore subberthehut should be considered obsolete and this repository will be archived. 4 | 5 | # subberthehut 6 | subberthehut is a command-line based OpenSubtitles.org downloader, written in C. 7 | 8 | It is inspired by [subdl](http://code.google.com/p/subdl/), which is not developed anymore. 9 | In contrast to subdl, subberthehut can also do a name-based subtitle search in case the hash-based search returns no results. 10 | 11 | ### Dependencies 12 | - xmlrpc-c 13 | - glib2 14 | - zlib 15 | - bash-completion (make only) 16 | 17 | ### Installation and Usage 18 | $ make 19 | $ sudo make install 20 | 21 | Then use ```subberthehut --help``` for usage information. 22 | 23 | #### Arch Linux Package 24 | subberthehut is available in the Arch User Repository (AUR): 25 | 26 | https://aur.archlinux.org/packages/subberthehut/ 27 | -------------------------------------------------------------------------------- /bash_completion: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | _subberthehut() 4 | { 5 | local cur="${COMP_WORDS[COMP_CWORD]}" 6 | 7 | local opts="-h -v -l -L -a -n -f -o -O -s -t -e -q 8 | --help --version --lang --list-languages 9 | --always-ask --never-ask 10 | --force --hash-search-only --name-search-only 11 | --same-name --limit --no-exit-on-fail --quiet" 12 | 13 | if [[ $cur == -* ]]; then 14 | COMPREPLY=( $(compgen -W "$opts" -- $cur) ) 15 | else 16 | mapfile -t COMPREPLY < <(compgen -f -- "$cur") 17 | fi 18 | } 19 | 20 | complete -o plusdirs -F _subberthehut subberthehut 21 | -------------------------------------------------------------------------------- /subberthehut.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Marius Thesing 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU General Public License as published by 6 | * the Free Software Foundation, either version 2 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | #define _GNU_SOURCE 19 | #define _FILE_OFFSET_BITS 64 20 | 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include // uint64_t / PRIx64 28 | 29 | #include 30 | #include 31 | #include // g_base64_decode_step 32 | #include 33 | 34 | #define STH_XMLRPC_URL "https://api.opensubtitles.org/xml-rpc" 35 | #define LOGIN_LANGCODE "en" 36 | #define LOGIN_USER_AGENT "subberthehut v" VERSION 37 | 38 | #define ZLIB_CHUNK (64 * 1024) 39 | 40 | #define STH_XMLRPC_SIZE_LIMIT (10 * 1024 * 1024) 41 | 42 | #define HEADER_ID '#' 43 | #define HEADER_MATCHED_BY_HASH 'H' 44 | #define HEADER_LANG "Lng" 45 | #define HEADER_RELEASE_NAME "Release / File Name" 46 | 47 | #define SEP_VERTICAL "\342\224\202" 48 | #define SEP_HORIZONTAL "\342\224\200" 49 | #define SEP_CROSS "\342\224\274" 50 | #define SEP_UP_RIGHT "\342\224\224" 51 | 52 | /* __attribute__(cleanup) */ 53 | #define _cleanup_free_ __attribute__((cleanup(cleanup_free))) 54 | #define _cleanup_fclose_ __attribute__((cleanup(cleanup_fclose))) 55 | #define _cleanup_xmlrpc_ __attribute__((cleanup(cleanup_xmlrpc_DECREF))) 56 | 57 | static void cleanup_free(void *p) { 58 | free(*(void**)p); 59 | } 60 | 61 | static void cleanup_fclose(FILE **p) { 62 | if(*p) 63 | fclose(*p); 64 | } 65 | 66 | static void cleanup_xmlrpc_DECREF(xmlrpc_value **p) { 67 | if(*p) 68 | xmlrpc_DECREF(*p); 69 | } 70 | /* end __attribute__(cleanup) */ 71 | 72 | static xmlrpc_env env; 73 | static xmlrpc_client *client; 74 | 75 | // options default values 76 | static const char *lang = "eng"; 77 | static bool list_languages = false; 78 | static bool force_overwrite = false; 79 | static bool always_ask = false; 80 | static bool never_ask = false; 81 | static bool hash_search_only = false; 82 | static bool name_search_only = false; 83 | static bool same_name = false; 84 | static int limit = 10; 85 | static bool exit_on_fail = true; 86 | static unsigned int quiet = 0; 87 | 88 | struct sub_info { 89 | int id; 90 | bool matched_by_hash; 91 | const char *lang; 92 | const char *release_name; 93 | const char *filename; 94 | }; 95 | 96 | static void log_err(const char *format, ...) { 97 | va_list args; 98 | va_start(args, format); 99 | vfprintf(stderr, format, args); 100 | va_end(args); 101 | putc('\n', stderr); 102 | } 103 | 104 | static void log_info(const char *format, ...) { 105 | if (quiet >= 2) 106 | return; 107 | 108 | va_list args; 109 | va_start(args, format); 110 | vprintf(format, args); 111 | va_end(args); 112 | putchar('\n'); 113 | } 114 | 115 | static int log_oom() { 116 | log_err("Out of Memory."); 117 | return ENOMEM; 118 | } 119 | 120 | /* 121 | * creates the 64-bit hash used for the search query. 122 | * copied and modified from: 123 | * http://trac.opensubtitles.org/projects/opensubtitles/wiki/HashSourceCodes 124 | */ 125 | static void get_hash_and_filesize(FILE *handle, uint64_t *hash, uint64_t *filesize) { 126 | fseek(handle, 0, SEEK_END); 127 | *filesize = ftell(handle); 128 | fseek(handle, 0, SEEK_SET); 129 | 130 | *hash = *filesize; 131 | 132 | for (uint64_t tmp = 0, i = 0; i < 65536 / sizeof(tmp) && fread((char *) &tmp, sizeof(tmp), 1, handle); *hash += tmp, i++); 133 | fseek(handle, (*filesize - 65536) > 0 ? (*filesize - 65536) : 0, SEEK_SET); 134 | for (uint64_t tmp = 0, i = 0; i < 65536 / sizeof(tmp) && fread((char *) &tmp, sizeof(tmp), 1, handle); *hash += tmp, i++); 135 | } 136 | 137 | /* 138 | * convenience function the get a string value from a xmlrpc struct. 139 | */ 140 | static const char *struct_get_string(xmlrpc_value *s, const char *key) { 141 | _cleanup_xmlrpc_ xmlrpc_value *xmlval = NULL; 142 | const char *str; 143 | 144 | xmlrpc_struct_find_value(&env, s, key, &xmlval); 145 | xmlrpc_read_string(&env, xmlval, &str); 146 | 147 | return str; 148 | } 149 | 150 | static int login(const char **token) { 151 | _cleanup_xmlrpc_ xmlrpc_value *result = NULL; 152 | _cleanup_xmlrpc_ xmlrpc_value *token_xmlval = NULL; 153 | _cleanup_free_ const char *status = NULL; 154 | 155 | xmlrpc_client_call2f(&env, client, STH_XMLRPC_URL, "LogIn", &result, "(ssss)", "", "", LOGIN_LANGCODE, LOGIN_USER_AGENT); 156 | if (env.fault_occurred) { 157 | log_err("login failed: %s (%d)", env.fault_string, env.fault_code); 158 | return env.fault_code; 159 | } 160 | 161 | status = struct_get_string(result, "status"); 162 | if (strcmp(status, "200 OK")) { 163 | log_err("login failed: %s", status); 164 | return 1; 165 | } 166 | 167 | xmlrpc_struct_find_value(&env, result, "token", &token_xmlval); 168 | xmlrpc_read_string(&env, token_xmlval, token); 169 | 170 | return 0; 171 | } 172 | 173 | static int search_get_results(const char *token, uint64_t hash, uint64_t filesize, 174 | const char *filename, xmlrpc_value **data) { 175 | _cleanup_xmlrpc_ xmlrpc_value *hash_query = NULL; 176 | _cleanup_xmlrpc_ xmlrpc_value *sublanguageid_xmlval = NULL; 177 | _cleanup_xmlrpc_ xmlrpc_value *hash_xmlval = NULL; 178 | _cleanup_xmlrpc_ xmlrpc_value *filesize_xmlval = NULL; 179 | _cleanup_free_ char *hash_str = NULL; 180 | _cleanup_free_ char *filesize_str = NULL; 181 | 182 | _cleanup_xmlrpc_ xmlrpc_value *name_query = NULL; 183 | _cleanup_xmlrpc_ xmlrpc_value *filename_xmlval = NULL; 184 | 185 | _cleanup_xmlrpc_ xmlrpc_value *query_array = NULL; 186 | 187 | _cleanup_xmlrpc_ xmlrpc_value *limit_xmlval = NULL; 188 | _cleanup_xmlrpc_ xmlrpc_value *param_struct = NULL; 189 | 190 | _cleanup_xmlrpc_ xmlrpc_value *result = NULL; 191 | 192 | query_array = xmlrpc_array_new(&env); 193 | 194 | // create hash-based query 195 | if (!name_search_only) { 196 | hash_query = xmlrpc_struct_new(&env); 197 | sublanguageid_xmlval = xmlrpc_string_new(&env, lang); 198 | xmlrpc_struct_set_value(&env, hash_query, "sublanguageid", sublanguageid_xmlval); 199 | int r = asprintf(&hash_str, "%016" PRIx64, hash); 200 | if (r == -1) 201 | return log_oom(); 202 | 203 | hash_xmlval = xmlrpc_string_new(&env, hash_str); 204 | xmlrpc_struct_set_value(&env, hash_query, "moviehash", hash_xmlval); 205 | 206 | r = asprintf(&filesize_str, "%" PRIu64, filesize); 207 | if (r == -1) 208 | return log_oom(); 209 | 210 | filesize_xmlval = xmlrpc_string_new(&env, filesize_str); 211 | xmlrpc_struct_set_value(&env, hash_query, "moviebytesize", filesize_xmlval); 212 | 213 | xmlrpc_array_append_item(&env, query_array, hash_query); 214 | } 215 | 216 | // create full-text query 217 | if (!hash_search_only) { 218 | name_query = xmlrpc_struct_new(&env); 219 | 220 | sublanguageid_xmlval = xmlrpc_string_new(&env, lang); 221 | xmlrpc_struct_set_value(&env, name_query, "sublanguageid", sublanguageid_xmlval); 222 | 223 | filename_xmlval = xmlrpc_string_new(&env, filename); 224 | xmlrpc_struct_set_value(&env, name_query, "query", filename_xmlval); 225 | 226 | xmlrpc_array_append_item(&env, query_array, name_query); 227 | } 228 | 229 | // create parameter structure (currently only for "limit") 230 | param_struct = xmlrpc_struct_new(&env); 231 | limit_xmlval = xmlrpc_int_new(&env, limit); 232 | xmlrpc_struct_set_value(&env, param_struct, "limit", limit_xmlval); 233 | 234 | xmlrpc_client_call2f(&env, client, STH_XMLRPC_URL, "SearchSubtitles", &result, "(sAS)", token, query_array, param_struct); 235 | if (env.fault_occurred) { 236 | log_err("query failed: %s (%d)", env.fault_string, env.fault_code); 237 | return env.fault_code; 238 | } 239 | 240 | xmlrpc_struct_read_value(&env, result, "data", data); 241 | if (env.fault_occurred) { 242 | log_err("failed to get data: %s (%d)", env.fault_string, env.fault_code); 243 | return env.fault_code; 244 | } 245 | 246 | return 0; 247 | } 248 | 249 | static void print_separator(int c, int digit_count) { 250 | for (int i = 0; i < c; i++) { 251 | if (i == digit_count + 1 || 252 | i == digit_count + 1 + 4 || 253 | i == digit_count + 1 + 4 + 6) { 254 | fputs(SEP_CROSS, stdout); 255 | } 256 | else { 257 | fputs(SEP_HORIZONTAL, stdout); 258 | } 259 | } 260 | putchar('\n'); 261 | } 262 | 263 | static void print_table(struct sub_info *sub_infos, int n, int align_release_name) { 264 | // count number of digits 265 | int digit_count = 0; 266 | int n_tmp = n; 267 | while (n_tmp) { 268 | n_tmp /= 10; 269 | digit_count++; 270 | } 271 | 272 | // header 273 | putchar('\n'); 274 | int c = printf("%-*c " SEP_VERTICAL " %c " SEP_VERTICAL " %s " SEP_VERTICAL " %-*s\n", 275 | digit_count, 276 | HEADER_ID, 277 | HEADER_MATCHED_BY_HASH, 278 | HEADER_LANG, 279 | align_release_name, 280 | HEADER_RELEASE_NAME); 281 | 282 | c -= 5; 283 | 284 | // separator 285 | print_separator(c, digit_count); 286 | 287 | // list 288 | for (int i = 0; i < n; i++) { 289 | printf("%-*i " SEP_VERTICAL " %c " SEP_VERTICAL " %s " SEP_VERTICAL " %-*s\n", 290 | digit_count, 291 | i + 1, 292 | sub_infos[i].matched_by_hash ? '*' : ' ', 293 | sub_infos[i].lang, 294 | align_release_name, 295 | sub_infos[i].release_name); 296 | 297 | printf("%-*s " SEP_VERTICAL " " SEP_VERTICAL " " SEP_VERTICAL " " SEP_UP_RIGHT "%s\n", 298 | digit_count, 299 | "", 300 | sub_infos[i].filename); 301 | 302 | if (i != n - 1) 303 | print_separator(c, digit_count); 304 | } 305 | putchar('\n'); 306 | } 307 | 308 | static int choose_from_results(xmlrpc_value *results, int n, int *sub_id, const char **sub_filename) { 309 | int r = 0; 310 | struct sub_info sub_infos[n]; 311 | 312 | int sel = 0; // selected list item 313 | 314 | /* Make the values in the "Release / File Name" column 315 | * at least as long as the header title itself. */ 316 | int align_release_name = strlen(HEADER_RELEASE_NAME); 317 | 318 | for (int i = 0; i < n; i++) { 319 | _cleanup_xmlrpc_ xmlrpc_value *oneresult = NULL; 320 | xmlrpc_array_read_item(&env, results, i, &oneresult); 321 | 322 | // dear OpenSubtitles.org, why are these IDs provided as strings? 323 | _cleanup_free_ const char *sub_id_str = struct_get_string(oneresult, "IDSubtitleFile"); 324 | _cleanup_free_ const char *matched_by_str = struct_get_string(oneresult, "MatchedBy"); 325 | 326 | sub_infos[i].id = strtol(sub_id_str, NULL, 10); 327 | sub_infos[i].matched_by_hash = strcmp(matched_by_str, "moviehash") == 0; 328 | sub_infos[i].lang = struct_get_string(oneresult, "SubLanguageID"); 329 | sub_infos[i].release_name = struct_get_string(oneresult, "MovieReleaseName"); 330 | sub_infos[i].filename = struct_get_string(oneresult, "SubFileName"); 331 | 332 | // select first hash match if one exists 333 | if (sub_infos[i].matched_by_hash && sel == 0) 334 | sel = i + 1; 335 | 336 | int s = strlen(sub_infos[i].release_name); 337 | if (s > align_release_name) 338 | align_release_name = s; 339 | 340 | s = strlen(sub_infos[i].filename); 341 | if (s > align_release_name) 342 | align_release_name = s; 343 | } 344 | 345 | if (never_ask && sel == 0) 346 | sel = 1; 347 | 348 | if (sel == 0 || always_ask) { 349 | print_table(sub_infos, n, align_release_name); 350 | 351 | _cleanup_free_ char *line = NULL; 352 | size_t len = 0; 353 | char *endptr = NULL; 354 | do { 355 | printf("Choose subtitle [1..%i]: ", n); 356 | if (getline(&line, &len, stdin) == -1) { 357 | r = EIO; 358 | goto finish; 359 | } 360 | 361 | sel = strtol(line, &endptr, 10); 362 | } while (*endptr != '\n' || sel < 1 || sel > n); 363 | } 364 | else if (!quiet) { 365 | print_table(sub_infos, n, align_release_name); 366 | } 367 | 368 | *sub_id = sub_infos[sel - 1].id; 369 | *sub_filename = strdup(sub_infos[sel - 1].filename); 370 | 371 | if (!*sub_filename) { 372 | r = log_oom(); 373 | goto finish; 374 | } 375 | 376 | finish: 377 | // __attribute__(cleanup) can't be used in structs, let alone arrays 378 | for (int i = 0; i < n; i++) { 379 | free((void *)sub_infos[i].lang); 380 | free((void *)sub_infos[i].release_name); 381 | free((void *)sub_infos[i].filename); 382 | } 383 | 384 | return r; 385 | } 386 | 387 | static int sub_download(const char *token, int sub_id, const char *file_path) { 388 | _cleanup_xmlrpc_ xmlrpc_value *sub_id_xmlval = NULL; 389 | _cleanup_xmlrpc_ xmlrpc_value *query_array = NULL; 390 | _cleanup_xmlrpc_ xmlrpc_value *result = NULL;; 391 | 392 | _cleanup_xmlrpc_ xmlrpc_value *data = NULL; // result -> data 393 | _cleanup_xmlrpc_ xmlrpc_value *data_0 = NULL; // result -> data[0] 394 | _cleanup_xmlrpc_ xmlrpc_value *data_0_sub = NULL; // result -> data[0][data] 395 | 396 | _cleanup_free_ const char *sub_base64 = NULL; // the subtitle, gzipped and base64 encoded 397 | 398 | // zlib stuff, see also http://zlib.net/zlib_how.html 399 | int z_ret; 400 | z_stream z_strm; 401 | unsigned char z_out[ZLIB_CHUNK]; 402 | unsigned char z_in[ZLIB_CHUNK]; 403 | z_strm.zalloc = Z_NULL; 404 | z_strm.zfree = Z_NULL; 405 | z_strm.opaque = Z_NULL; 406 | z_strm.avail_in = 0; 407 | z_strm.next_in = Z_NULL; 408 | 409 | _cleanup_fclose_ FILE *f = NULL; 410 | int r = 0; 411 | 412 | // check if file already exists 413 | if (access(file_path, F_OK) == 0) { 414 | if (force_overwrite) { 415 | log_info("file already exists, overwriting."); 416 | } else { 417 | log_err("file already exists, aborting. Use -f to force an overwrite."); 418 | return EEXIST; 419 | } 420 | } 421 | 422 | // download 423 | sub_id_xmlval = xmlrpc_int_new(&env, sub_id); 424 | 425 | query_array = xmlrpc_array_new(&env); 426 | xmlrpc_array_append_item(&env, query_array, sub_id_xmlval); 427 | 428 | xmlrpc_client_call2f(&env, client, STH_XMLRPC_URL, "DownloadSubtitles", &result, "(sA)", token, query_array); 429 | if (env.fault_occurred) { 430 | log_err("query failed: %s (%d)", env.fault_string, env.fault_code); 431 | return env.fault_code; 432 | } 433 | 434 | // get base64 encoded data 435 | xmlrpc_struct_find_value(&env, result, "data", &data); 436 | xmlrpc_array_read_item(&env, data, 0, &data_0); 437 | xmlrpc_struct_find_value(&env, data_0, "data", &data_0_sub); 438 | xmlrpc_read_string(&env, data_0_sub, &sub_base64); 439 | 440 | // decode and decompress to file 441 | f = fopen(file_path, "w+"); 442 | if (!f) { 443 | perror("failed to open output file"); 444 | return errno; 445 | } 446 | 447 | // 16+MAX_WBITS is needed for gzip support 448 | z_ret = inflateInit2(&z_strm, 16 + MAX_WBITS); 449 | if (z_ret != Z_OK) { 450 | log_err("failed to init zlib (%i)", z_ret); 451 | return z_ret; 452 | } 453 | 454 | int b64_state = 0; 455 | unsigned int b64_save = 0; 456 | unsigned int b64_offset = 0; 457 | do { 458 | // write decoded data to z_in 459 | z_strm.avail_in = g_base64_decode_step(&sub_base64[b64_offset], ZLIB_CHUNK, z_in, &b64_state, &b64_save); 460 | b64_offset += z_strm.avail_in * 4 / 3; // base64 encodes 3 bytes in 4 chars 461 | if (z_strm.avail_in == 0) 462 | break; 463 | 464 | z_strm.next_in = z_in; 465 | 466 | // decompress decoded data from z_in to z_out 467 | do { 468 | z_strm.avail_out = ZLIB_CHUNK; 469 | z_strm.next_out = z_out; 470 | z_ret = inflate(&z_strm, Z_NO_FLUSH); 471 | 472 | switch (z_ret) { 473 | case Z_NEED_DICT: 474 | z_ret = Z_DATA_ERROR; 475 | // fallthrough 476 | case Z_DATA_ERROR: 477 | case Z_MEM_ERROR: 478 | r = z_ret; 479 | log_err("zlib error: %s (%d)", z_strm.msg, z_ret); 480 | goto finish; 481 | } 482 | // write decompressed data from z_out to file 483 | unsigned int have = ZLIB_CHUNK - z_strm.avail_out; 484 | 485 | size_t written = fwrite(z_out, 1, have, f); 486 | if (written != have) { 487 | log_err("failed to write file: %m"); 488 | r = errno; 489 | goto finish; 490 | } 491 | } while (z_strm.avail_out == 0); 492 | } while (z_ret != Z_STREAM_END); 493 | 494 | finish: 495 | inflateEnd(&z_strm); 496 | 497 | return r; 498 | } 499 | 500 | static void show_usage() { 501 | puts("Usage: subberthehut [options] ...\n\n" 502 | 503 | "OpenSubtitles.org downloader.\n\n" 504 | 505 | "subberthehut can do a hash-based and a name-based search.\n" 506 | "On a hash-based search, subberthehut will generate a hash from the specified\n" 507 | "video file and use this to search for appropriate subtitles.\n" 508 | "Any results from this hash-based search should be compatible\n" 509 | "with the video file. Therefore subberthehut will, by default, automatically\n" 510 | "download the first subtitle from these search results.\n" 511 | "In case the hash-based search returns no results, subberthehut will also\n" 512 | "do a name-based search, meaning the OpenSubtitles.org database\n" 513 | "will be searched with the filename of the specified file. The results\n" 514 | "from this search are not guaranteed to be compatible with the video\n" 515 | "file. Therefore subberthehut will, by default, ask the user which subtitle to\n" 516 | "download.\n" 517 | "Results from the hash-based search are marked with an asterisk (*)\n" 518 | "in the 'H' column.\n\n" 519 | 520 | "Options:\n" 521 | " -h, --help Show this help and exit.\n" 522 | "\n" 523 | " -v, --version Show version information and exit.\n" 524 | "\n" 525 | " -l, --lang Comma-separated list of languages to search for,\n" 526 | " e.g. 'eng,ger'. Use 'all' to search for all\n" 527 | " languages. Default is 'eng'. Use --list-languages\n" 528 | " to list all available languages.\n" 529 | "\n" 530 | " -L, --list-languages List all available languages and exit.\n" 531 | "\n" 532 | " -a, --always-ask Always ask which subtitle to download, even\n" 533 | " when there are hash-based results.\n" 534 | "\n" 535 | " -n, --never-ask Never ask which subtitle to download, even\n" 536 | " when there are only name-based results.\n" 537 | " When this option is specified, the first\n" 538 | " search result will be downloaded.\n" 539 | "\n" 540 | " -f, --force Overwrite output file if it already exists.\n" 541 | "\n" 542 | " -o, --hash-search-only Only do a hash-based search.\n" 543 | "\n" 544 | " -O, --name-search-only Only do a name-based search. This is useful in\n" 545 | " case of false positives from the hash-based search.\n" 546 | "\n" 547 | " -s, --same-name Download the subtitle to the same filename as the\n" 548 | " original file, only replacing the file extension.\n" 549 | "\n" 550 | " -t, --limit Limits the number of returned results. The default is 10.\n" 551 | "\n" 552 | " -e, --no-exit-on-fail By default, subberthehut will exit immediately if\n" 553 | " multiple files are passed and it fails to download\n" 554 | " a subtitle for one them. When this option is passed,\n" 555 | " subberthehut will process the next file(s) regardless.\n" 556 | "\n" 557 | " -q, --quiet Don't print the table if the user doesn't have to be\n" 558 | " asked which subtitle to download. Pass this option twice\n" 559 | " to suppress anything but warnings and error messages.\n"); 560 | } 561 | 562 | static void show_version() { 563 | puts("subberthehut " VERSION "\n" 564 | "https://github.com/mus65/subberthehut/"); 565 | } 566 | 567 | static const char *get_sub_path(const char *filepath, const char *sub_filename) { 568 | char *sub_filepath; 569 | 570 | if (same_name) { 571 | const char *sub_ext = strrchr(sub_filename, '.'); 572 | if (!sub_ext) { 573 | log_err("warning: subtitle filename from the OpenSubtitles.org " 574 | "database has no file extension, assuming .srt."); 575 | sub_ext = ".srt"; 576 | } 577 | const char *lastdot = strrchr(filepath, '.'); 578 | int index; 579 | if (!lastdot) 580 | index = strlen(filepath) - 1; 581 | else 582 | index = (lastdot - filepath); 583 | 584 | sub_filepath = malloc(index + 1 + strlen(sub_ext) + 1); 585 | if (!sub_filepath) 586 | return NULL; 587 | 588 | strncpy(sub_filepath, filepath, index); 589 | sub_filepath[index] = '\0'; 590 | strcat(sub_filepath, sub_ext); 591 | } else { 592 | const char *lastslash = strrchr(filepath, '/'); 593 | if (!lastslash) { 594 | sub_filepath = strdup(sub_filename); 595 | if (!sub_filepath) 596 | return NULL; 597 | } else { 598 | int index = (lastslash - filepath); 599 | sub_filepath = malloc(index + 1 + strlen(sub_filename) + 1); 600 | if (!sub_filepath) 601 | return NULL; 602 | 603 | strncpy(sub_filepath, filepath, index + 1); 604 | sub_filepath[index + 1] = '\0'; 605 | strcat(sub_filepath, sub_filename); 606 | } 607 | } 608 | return sub_filepath; 609 | } 610 | 611 | static int process_file(const char *filepath, const char *token) { 612 | _cleanup_fclose_ FILE *f = NULL; 613 | uint64_t hash = 0, filesize = 0; 614 | 615 | _cleanup_xmlrpc_ xmlrpc_value *results = NULL; 616 | 617 | _cleanup_free_ const char *sub_filename = NULL; 618 | _cleanup_free_ const char *sub_filepath = NULL; 619 | 620 | int r = 0; 621 | 622 | // get hash/filesize 623 | if (!name_search_only) { 624 | f = fopen(filepath, "r"); 625 | if (!f) { 626 | log_err("failed to open %s: %m", filepath); 627 | return errno; 628 | } 629 | 630 | get_hash_and_filesize(f, &hash, &filesize); 631 | } 632 | 633 | const char *filename = strrchr(filepath, '/'); 634 | if (filename) 635 | filename++; // skip '/' 636 | else 637 | filename = filepath; 638 | 639 | log_info("searching for %s...", filename); 640 | 641 | r = search_get_results(token, hash, filesize, filename, &results); 642 | if (r != 0) 643 | return r; 644 | 645 | int results_length = xmlrpc_array_size(&env, results); 646 | if (env.fault_occurred) { 647 | log_err("failed to get array size: %s (%d)", env.fault_string, env.fault_code); 648 | return env.fault_code; 649 | } 650 | 651 | if (results_length == 0) { 652 | log_err("no results."); 653 | return 1; 654 | } 655 | 656 | // let user choose the subtitle to download 657 | int sub_id = 0; 658 | r = choose_from_results(results, results_length, &sub_id, &sub_filename); 659 | if (r != 0) 660 | return r; 661 | 662 | sub_filepath = get_sub_path(filepath, sub_filename); 663 | if (!sub_filepath) 664 | return log_oom(); 665 | 666 | log_info("downloading to %s ...", sub_filepath); 667 | r = sub_download(token, sub_id, sub_filepath); 668 | 669 | return r; 670 | } 671 | 672 | static int list_sub_languages() { 673 | _cleanup_xmlrpc_ xmlrpc_value *result = NULL; 674 | _cleanup_xmlrpc_ xmlrpc_value *languages = NULL; 675 | 676 | xmlrpc_client_call2f(&env, client, STH_XMLRPC_URL, "GetSubLanguages", &result, "()"); 677 | if (env.fault_occurred) { 678 | log_err("failed to download languages: %s (%d)", env.fault_string, env.fault_code); 679 | return env.fault_code; 680 | } 681 | 682 | xmlrpc_struct_read_value(&env, result, "data", &languages); 683 | if (env.fault_occurred) { 684 | log_err("failed to get data: %s (%d)", env.fault_string, env.fault_code); 685 | return env.fault_code; 686 | } 687 | 688 | int n = xmlrpc_array_size(&env, languages); 689 | if (env.fault_occurred) { 690 | log_err("failed to get array size: %s (%d)", env.fault_string, env.fault_code); 691 | return env.fault_code; 692 | } 693 | 694 | for (int i = 0; i < n; i++) { 695 | _cleanup_xmlrpc_ xmlrpc_value *language = NULL; 696 | xmlrpc_array_read_item(&env, languages, i, &language); 697 | 698 | _cleanup_free_ const char *lang_id = struct_get_string(language, "SubLanguageID"); 699 | _cleanup_free_ const char *lang_name = struct_get_string(language, "LanguageName"); 700 | 701 | printf("%s - %s\n", lang_id, lang_name); 702 | } 703 | 704 | return 0; 705 | } 706 | 707 | int main(int argc, char *argv[]) { 708 | _cleanup_free_ const char *token = NULL; 709 | 710 | int r = EXIT_SUCCESS; 711 | 712 | // parse options 713 | const struct option opts[] = { 714 | {"help", no_argument, NULL, 'h'}, 715 | {"lang", required_argument, NULL, 'l'}, 716 | {"list-languages", no_argument, NULL, 'L'}, 717 | {"always-ask", no_argument, NULL, 'a'}, 718 | {"never-ask", no_argument, NULL, 'n'}, 719 | {"force", no_argument, NULL, 'f'}, 720 | {"hash-search-only", no_argument, NULL, 'o'}, 721 | {"name-search-only", no_argument, NULL, 'O'}, 722 | {"same-name", no_argument, NULL, 's'}, 723 | {"limit", required_argument, NULL, 't'}, 724 | {"no-exit-on-fail", no_argument, NULL, 'e'}, 725 | {"quiet", no_argument, NULL, 'q'}, 726 | {"version", no_argument, NULL, 'v'}, 727 | {0, 0, 0, 0} 728 | }; 729 | 730 | int c; 731 | while ((c = getopt_long(argc, argv, "hl:LanfoOst:eqv", opts, NULL)) != -1) { 732 | switch (c) { 733 | case 'h': 734 | show_usage(); 735 | return EXIT_SUCCESS; 736 | 737 | case 'l': 738 | lang = optarg; 739 | break; 740 | 741 | case 'L': 742 | list_languages = true; 743 | break; 744 | 745 | case 'a': 746 | always_ask = true; 747 | break; 748 | 749 | case 'n': 750 | never_ask = true; 751 | break; 752 | 753 | case 'f': 754 | force_overwrite = true; 755 | break; 756 | 757 | case 'o': 758 | hash_search_only = true; 759 | name_search_only = false; 760 | break; 761 | 762 | case 'O': 763 | name_search_only = true; 764 | hash_search_only = false; 765 | break; 766 | 767 | case 's': 768 | same_name = true; 769 | break; 770 | 771 | case 't': 772 | { 773 | char *endptr = NULL; 774 | limit = strtol(optarg, &endptr, 10); 775 | 776 | if (*endptr != '\0' || limit < 1) { 777 | log_err("invalid limit: %s", optarg); 778 | return EXIT_FAILURE; 779 | } 780 | break; 781 | } 782 | 783 | case 'e': 784 | exit_on_fail = false; 785 | break; 786 | 787 | case 'q': 788 | quiet++; 789 | break; 790 | 791 | case 'v': 792 | show_version(); 793 | return EXIT_SUCCESS; 794 | 795 | default: 796 | return EXIT_FAILURE; 797 | } 798 | } 799 | 800 | // check if user has specified at least one file (except for listing languages) 801 | if (argc - optind < 1 && !list_languages) { 802 | show_usage(); 803 | return EXIT_FAILURE; 804 | } 805 | 806 | // xmlrpc init 807 | xmlrpc_env_init(&env); 808 | xmlrpc_client_setup_global_const(&env); 809 | // make sure the library doesn't complain about too much data 810 | xmlrpc_limit_set(XMLRPC_XML_SIZE_LIMIT_ID, STH_XMLRPC_SIZE_LIMIT); 811 | 812 | xmlrpc_client_create(&env, XMLRPC_CLIENT_NO_FLAGS, "subberthehut", VERSION, NULL, 0, &client); 813 | if (env.fault_occurred) { 814 | log_err("failed to init xmlrpc client: %s (%d)", env.fault_string, env.fault_code); 815 | r = env.fault_code; 816 | goto finish; 817 | } 818 | 819 | // login 820 | r = login(&token); 821 | if (r != 0) 822 | goto finish; 823 | 824 | // only list the languages and exit 825 | if (list_languages) { 826 | r = list_sub_languages(); 827 | goto finish; 828 | } 829 | 830 | // process files 831 | for (int i = optind; i < argc; i++) { 832 | char *filepath = argv[i]; 833 | r = process_file(filepath, token); 834 | if (r != 0 && exit_on_fail) 835 | goto finish; 836 | } 837 | 838 | finish: 839 | xmlrpc_env_clean(&env); 840 | xmlrpc_client_destroy(client); 841 | xmlrpc_client_teardown_global_const(); 842 | 843 | return r; 844 | } 845 | --------------------------------------------------------------------------------