├── .github └── workflows │ └── rust.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── Cargo.lock ├── Cargo.toml ├── Formula └── jellyroller.rb ├── LICENSE ├── README.md ├── bin ├── auto-pr.ps1 ├── checkhashes.ps1 ├── checkurls.ps1 ├── checkver.ps1 ├── formatjson.ps1 └── missing-checkver.ps1 ├── bucket └── jellyroller.json └── src ├── CHANGELOG.md ├── entities ├── activity_details.rs ├── device_details.rs ├── library_details.rs ├── log_details.rs ├── media_details.rs ├── mod.rs ├── movie_details.rs ├── package_details.rs ├── plugin_details.rs ├── repository_details.rs ├── server_details.rs ├── server_info.rs ├── task_details.rs ├── token_details.rs └── user_details.rs ├── main.rs ├── plugin_actions.rs ├── responder.rs ├── system_actions.rs ├── user_actions.rs └── utils ├── mod.rs ├── output_writer.rs └── status_handler.rs /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. 2 | # They are provided by a third-party and are governed by 3 | # separate terms of service, privacy policy, and support 4 | # documentation. 5 | # rust-clippy is a tool that runs a bunch of lints to catch common 6 | # mistakes in your Rust code and help improve your Rust code. 7 | # More details at https://github.com/rust-lang/rust-clippy 8 | # and https://rust-lang.github.io/rust-clippy/ 9 | 10 | name: Build and Check 11 | 12 | on: 13 | push: 14 | branches: [ "main" ] 15 | pull_request: 16 | # The branches below must be a subset of the branches above 17 | branches: [ "main" ] 18 | schedule: 19 | - cron: '35 6 * * 0' 20 | 21 | jobs: 22 | build: 23 | runs-on: ubuntu-latest 24 | 25 | steps: 26 | - uses: actions/checkout@v3 27 | - name: Build 28 | run: cargo build --verbose 29 | - name: Run tests 30 | run: cargo test --verbose 31 | 32 | rust-clippy-analyze: 33 | name: Run rust-clippy analyzing 34 | runs-on: ubuntu-latest 35 | permissions: 36 | contents: read 37 | security-events: write 38 | actions: read # only required for a private repository by github/codeql-action/upload-sarif to get the Action run status 39 | steps: 40 | - name: Checkout code 41 | uses: actions/checkout@v2 42 | 43 | - name: Install Rust toolchain 44 | uses: actions-rs/toolchain@16499b5e05bf2e26879000db0c1d13f7e13fa3af #@v1 45 | with: 46 | profile: minimal 47 | toolchain: stable 48 | components: clippy 49 | override: true 50 | 51 | - name: Install required cargo 52 | run: cargo install clippy-sarif sarif-fmt 53 | 54 | - name: Run rust-clippy 55 | run: 56 | cargo clippy 57 | --all-features 58 | --message-format=json | clippy-sarif | tee rust-clippy-results.sarif | sarif-fmt 59 | continue-on-error: true 60 | 61 | - name: Upload analysis results to GitHub 62 | uses: github/codeql-action/upload-sarif@v2 63 | with: 64 | sarif_file: rust-clippy-results.sarif 65 | wait-for-processing: true 66 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | *.code-workspace 3 | exported* 4 | /.idea -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | 2 | # Contributor Covenant Code of Conduct 3 | 4 | ## Our Pledge 5 | 6 | We as members, contributors, and leaders pledge to make participation in our 7 | community a harassment-free experience for everyone, regardless of age, body 8 | size, visible or invisible disability, ethnicity, sex characteristics, gender 9 | identity and expression, level of experience, education, socio-economic status, 10 | nationality, personal appearance, race, caste, color, religion, or sexual 11 | identity and orientation. 12 | 13 | We pledge to act and interact in ways that contribute to an open, welcoming, 14 | diverse, inclusive, and healthy community. 15 | 16 | ## Our Standards 17 | 18 | Examples of behavior that contributes to a positive environment for our 19 | community include: 20 | 21 | * Demonstrating empathy and kindness toward other people 22 | * Being respectful of differing opinions, viewpoints, and experiences 23 | * Giving and gracefully accepting constructive feedback 24 | * Accepting responsibility and apologizing to those affected by our mistakes, 25 | and learning from the experience 26 | * Focusing on what is best not just for us as individuals, but for the overall 27 | community 28 | 29 | Examples of unacceptable behavior include: 30 | 31 | * The use of sexualized language or imagery, and sexual attention or advances of 32 | any kind 33 | * Trolling, insulting or derogatory comments, and personal or political attacks 34 | * Public or private harassment 35 | * Publishing others' private information, such as a physical or email address, 36 | without their explicit permission 37 | * Other conduct which could reasonably be considered inappropriate in a 38 | professional setting 39 | 40 | ## Enforcement Responsibilities 41 | 42 | Community leaders are responsible for clarifying and enforcing our standards of 43 | acceptable behavior and will take appropriate and fair corrective action in 44 | response to any behavior that they deem inappropriate, threatening, offensive, 45 | or harmful. 46 | 47 | Community leaders have the right and responsibility to remove, edit, or reject 48 | comments, commits, code, wiki edits, issues, and other contributions that are 49 | not aligned to this Code of Conduct, and will communicate reasons for moderation 50 | decisions when appropriate. 51 | 52 | ## Scope 53 | 54 | This Code of Conduct applies within all community spaces, and also applies when 55 | an individual is officially representing the community in public spaces. 56 | Examples of representing our community include using an official e-mail address, 57 | posting via an official social media account, or acting as an appointed 58 | representative at an online or offline event. 59 | 60 | ## Enforcement 61 | 62 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 63 | reported to the community leaders responsible for enforcement at 64 | [INSERT CONTACT METHOD]. 65 | All complaints will be reviewed and investigated promptly and fairly. 66 | 67 | All community leaders are obligated to respect the privacy and security of the 68 | reporter of any incident. 69 | 70 | ## Enforcement Guidelines 71 | 72 | Community leaders will follow these Community Impact Guidelines in determining 73 | the consequences for any action they deem in violation of this Code of Conduct: 74 | 75 | ### 1. Correction 76 | 77 | **Community Impact**: Use of inappropriate language or other behavior deemed 78 | unprofessional or unwelcome in the community. 79 | 80 | **Consequence**: A private, written warning from community leaders, providing 81 | clarity around the nature of the violation and an explanation of why the 82 | behavior was inappropriate. A public apology may be requested. 83 | 84 | ### 2. Warning 85 | 86 | **Community Impact**: A violation through a single incident or series of 87 | actions. 88 | 89 | **Consequence**: A warning with consequences for continued behavior. No 90 | interaction with the people involved, including unsolicited interaction with 91 | those enforcing the Code of Conduct, for a specified period of time. This 92 | includes avoiding interactions in community spaces as well as external channels 93 | like social media. Violating these terms may lead to a temporary or permanent 94 | ban. 95 | 96 | ### 3. Temporary Ban 97 | 98 | **Community Impact**: A serious violation of community standards, including 99 | sustained inappropriate behavior. 100 | 101 | **Consequence**: A temporary ban from any sort of interaction or public 102 | communication with the community for a specified period of time. No public or 103 | private interaction with the people involved, including unsolicited interaction 104 | with those enforcing the Code of Conduct, is allowed during this period. 105 | Violating these terms may lead to a permanent ban. 106 | 107 | ### 4. Permanent Ban 108 | 109 | **Community Impact**: Demonstrating a pattern of violation of community 110 | standards, including sustained inappropriate behavior, harassment of an 111 | individual, or aggression toward or disparagement of classes of individuals. 112 | 113 | **Consequence**: A permanent ban from any sort of public interaction within the 114 | community. 115 | 116 | ## Attribution 117 | 118 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 119 | version 2.1, available at 120 | [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. 121 | 122 | Community Impact Guidelines were inspired by 123 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. 124 | 125 | For answers to common questions about this code of conduct, see the FAQ at 126 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at 127 | [https://www.contributor-covenant.org/translations][translations]. 128 | 129 | [homepage]: https://www.contributor-covenant.org 130 | [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html 131 | [Mozilla CoC]: https://github.com/mozilla/diversity 132 | [FAQ]: https://www.contributor-covenant.org/faq 133 | [translations]: https://www.contributor-covenant.org/translations 134 | 135 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "jellyroller" 3 | version = "0.8.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | chrono = "0.4.39" 10 | clap = { version = "4.3.19", features = ["derive"] } 11 | confy = "0.6.0" 12 | serde = "1.0.144" 13 | serde_derive = "1.0.144" 14 | serde_json = "1.0.85" 15 | reqwest = { version = "0.12.4", features = ["blocking", "json", "rustls-tls"], default-features = false } 16 | rpassword = "7.3.1" 17 | url = "2.2.2" 18 | comfy-table = "7.0.1" 19 | image = "0.25.1" 20 | base64 = "0.22.1" 21 | csv = "1.3.1" 22 | clap_complete = "4.5.42" 23 | -------------------------------------------------------------------------------- /Formula/jellyroller.rb: -------------------------------------------------------------------------------- 1 | class Jellyroller < Formula 2 | desc "CLI Jellyfin Controller Utility for Linux and Windows" 3 | homepage "" 4 | url "https://github.com/LSchallot/JellyRoller/archive/refs/tags/v0.7.0.tar.gz" 5 | sha256 "3a3b47b98260cb76fb6976ceab2ab77f88c94fd415bdceda0dffa24d1be9f72a" 6 | license "GPL-2.0" 7 | 8 | depends_on "rust" => :build 9 | 10 | def install 11 | system "cargo", "install", *std_cargo_args 12 | end 13 | 14 | bottle do 15 | root_url "https://github.com/LSchallot/JellyRoller/releases/download/v0.7.0" 16 | sha256 x86_64_linux: "94bba98e71b3661ebc4b9cf7d5c196b8aa9bf4f39ab1392c059a33cbb8f72020" 17 | end 18 | 19 | test do 20 | system "false" 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /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 | {{description}} 294 | Copyright (C) {{year}} {{fullname}} 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 | {signature of Ty Coon}, 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JellyRoller - The CLI Jellyfin Controller Utility for Linux and Windows 2 | 3 | JellyRoller is an open source CLI Jellyfin Controller written in Rust that works on Windows and Linux. Its primary purpose is to allow administration of a Jellyfin application from the command line. 4 | 5 | ## How it works 6 | On the first execution, JellyRoller prompts for information to authenticate as an admin user. Once this authentication has succeeded, an API key is created and stored within the JellyrRoller configuration. JellyRoller then uses the Jellyfin API to manage the server. 7 | 8 | Any previous user auth tokens will be converted to an API key upon next execution when upgrading from JellyRoller < 0.3. 9 | 10 | ## Usage Information 11 | 12 | ``` 13 | jellyroller 0.8.0 14 | A CLI controller for managing Jellyfin 15 | 16 | Usage: jellyroller.exe 17 | 18 | Commands: 19 | add-user Creates a new user 20 | add-users Uses the supplied file to mass create new users 21 | create-report Creates a report of either activity or available movie items 22 | delete-user Deletes an existing user 23 | disable-user Disable a user 24 | enable-user Enable a user 25 | execute-task-by-name Executes a scheduled task by name 26 | generate-report Generate a report for an issue 27 | get-devices Show all devices 28 | get-libraries Gets the libraries available to the configured user 29 | get-packages Lists all available packages 30 | get-plugins Returns a list of installed plugins 31 | get-repositories Lists all current repositories 32 | get-scheduled-tasks Show all scheduled tasks and their status 33 | grant-admin Grants the specified user admin rights 34 | install-package Installs the specified package 35 | list-logs Displays the available system logs 36 | list-users Lists the current users with basic information 37 | reconfigure Reconfigure the connection information 38 | register-library Registers a new library 39 | register-repository Registers a new Plugin Repository 40 | remove-device-by-username Removes all devices associated with the specified user 41 | reset-password Resets a user's password 42 | revoke-admin Revokes admin rights from the specified user 43 | restart-jellyfin Restarts Jellyfin 44 | scan-library Start a library scan 45 | search-media Executes a search of your media 46 | server-info Displays the server information 47 | show-log Displays the requested logfile 48 | shutdown-jellyfin Shuts down Jellyfin 49 | update-image-by-id Updates image of specified file by id 50 | update-image-by-name Updates image of specified file by name 51 | update-metadata Updates metadata of specified id with metadata provided by specified file 52 | update-users Mass update users in the supplied file 53 | help Print this message or the help of the given subcommand(s) 54 | 55 | Options: 56 | -h, --help Print help 57 | -V, --version Print version 58 | 59 | ``` 60 | 61 | ## Installation 62 | 63 | **Note:** All installation instructions assume the end-user can handle adding the application to their user's PATH. 64 | 65 | ### Mac / Linux (Homebrew) 66 | ``` 67 | brew tap LSchallot/JellyRoller https://github.com/LSchallot/JellyRoller 68 | ``` 69 | #### (Linux) 70 | ``` 71 | brew install jellyroller 72 | ``` 73 | #### (Mac) 74 | ``` 75 | brew install --build-from-source jellyroller 76 | ``` 77 | ### Windows (Scoop) 78 | ``` 79 | scoop add bucket jellyroller https://github.com/lschallot/jellyroller.git 80 | scoop update 81 | scoop install jellyroller 82 | ``` 83 | 84 | ### Building From Source 85 | 86 | Currently built with rustc 1.85.0. If building on a Linux machine, you may need to install openssl-devel. 87 | 88 | ``` 89 | cargo install --git https://github.com/LSchallot/JellyRoller 90 | ``` 91 | 92 | ### Initial Configuration 93 | 94 | When running JellyRoller for the first time, you will be prompted to configure against your Jellyfin instance. You will be prompted for various items which are described below. 95 | | Prompt | Description | 96 | | ------------- | ------------- | 97 | | Please enter your Jellyfin URL: | The URL to your Jellyfin instance. Depending on your setup, you may need to provide the port. Examples include http://myjellyfin.lab or http://localhost:8096. | 98 | | Please enter your Jellyfin username: | Username with admin rights that JellyRoller will use to execute commands. | 99 | | Please enter your Jellyfin password: | Password associated with the username being used. | 100 | 101 | ### Custom Configuration 102 | As of 0.5.0, it is possible to keep your configuration file alongside of the JellyRoller executable. Simply save your configuration in the same directory with the name "jellyroller.config" and it will be used automatically. Keep in mind that this configurtion file will contain your API key, so secure the file as needed. 103 | 104 | ### Downloading Release 105 | 106 | See Releases for binaries. I can currently supply builds for x86_64 Windows and x86_64 Linux. Please open an issue if you would like to request an additional format. 107 | 108 | ## Roadmap 109 | 110 | Please open issues for feature requests or enhancements. 111 | -------------------------------------------------------------------------------- /bin/auto-pr.ps1: -------------------------------------------------------------------------------- 1 | #Requires -Version 5.1 2 | param( 3 | # overwrite upstream param 4 | [String]$upstream = 'lschallot/jellyroller:master' 5 | ) 6 | 7 | if (!$env:SCOOP_HOME) { $env:SCOOP_HOME = Resolve-Path (scoop prefix scoop) } 8 | $autopr = "$env:SCOOP_HOME/bin/auto-pr.ps1" 9 | $dir = "$psscriptroot/../bucket" # checks the parent dir 10 | Invoke-Expression -Command "$autopr -dir $dir -upstream $upstream $($args | ForEach-Object { "$_ " })" 11 | -------------------------------------------------------------------------------- /bin/checkhashes.ps1: -------------------------------------------------------------------------------- 1 | #Requires -Version 5.1 2 | if (!$env:SCOOP_HOME) { $env:SCOOP_HOME = Resolve-Path (scoop prefix scoop) } 3 | $checkhashes = "$env:SCOOP_HOME\bin\checkhashes.ps1" 4 | $dir = "$psscriptroot\..\bucket" # checks the parent dir 5 | Invoke-Expression -Command "$checkhashes -dir $dir $($args | ForEach-Object { "$_ " })" 6 | -------------------------------------------------------------------------------- /bin/checkurls.ps1: -------------------------------------------------------------------------------- 1 | #Requires -Version 5.1 2 | if (!$env:SCOOP_HOME) { $env:SCOOP_HOME = Resolve-Path (scoop prefix scoop) } 3 | $checkurls = "$env:SCOOP_HOME\bin\checkurls.ps1" 4 | $dir = "$psscriptroot\..\bucket" # checks the parent dir 5 | Invoke-Expression -Command "$checkurls -dir $dir $($args | ForEach-Object { "$_ " })" 6 | -------------------------------------------------------------------------------- /bin/checkver.ps1: -------------------------------------------------------------------------------- 1 | #Requires -Version 5.1 2 | param( 3 | [String] $dir = "$PSScriptRoot\..\bucket", 4 | [Parameter(ValueFromRemainingArguments = $true)] 5 | [String[]] $remainArgs = @() 6 | ) 7 | 8 | if (!$env:SCOOP_HOME) { $env:SCOOP_HOME = Resolve-Path (scoop prefix scoop) } 9 | $checkver = "$env:SCOOP_HOME\bin\checkver.ps1" 10 | $remainArgs = ($remainArgs | Select-Object -Unique) -join ' ' 11 | 12 | Invoke-Expression -Command "$checkver -dir $dir $remainArgs" 13 | -------------------------------------------------------------------------------- /bin/formatjson.ps1: -------------------------------------------------------------------------------- 1 | #Requires -Version 5.1 2 | if (!$env:SCOOP_HOME) { $env:SCOOP_HOME = Resolve-Path (scoop prefix scoop) } 3 | $formatjson = "$env:SCOOP_HOME\bin\formatjson.ps1" 4 | $dir = "$psscriptroot\..\bucket" # checks the parent dir 5 | Invoke-Expression -Command "$formatjson -dir $dir $($args | ForEach-Object { "$_ " })" 6 | -------------------------------------------------------------------------------- /bin/missing-checkver.ps1: -------------------------------------------------------------------------------- 1 | #Requires -Version 5.1 2 | 3 | if (!$env:SCOOP_HOME) { $env:SCOOP_HOME = Resolve-Path (scoop prefix scoop) } 4 | $missing_checkver = "$env:SCOOP_HOME\bin\missing-checkver.ps1" 5 | $dir = "$psscriptroot\..\bucket" # checks the parent dir 6 | Invoke-Expression -Command "$missing_checkver -dir $dir $($args | ForEach-Object { "$_ " })" 7 | -------------------------------------------------------------------------------- /bucket/jellyroller.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.6.0", 3 | "description": "Jellyroller is a CLI configuration tool for Jellyfin", 4 | "homepage": "https://github.com/lschallot/jellyroller", 5 | "license": "GPL-2", 6 | "architecture": { 7 | "64bit": { 8 | "url": "https://github.com/LSchallot/JellyRoller/releases/download/v0.7.0/jellyroller-0.7.0-x86_64-windows.tar.gz", 9 | "hash": "db48d833596ddf939c3e59a7b165c77aa06384648eff4ac2df0517bb470b4bd8" 10 | } 11 | }, 12 | "bin": "jellyroller.exe", 13 | "checkver": { 14 | "url": "https://api.github.com/repos/lschallot/jellyroller/releases/latest", 15 | "regex": "/v([\\w-.]+)" 16 | }, 17 | "autoupdate": { 18 | "architecture": { 19 | "64bit": { 20 | "url": "https://github.com/lschallot/jellyroller/releases/download/v$version/jellyroller-$version-x86_64-windows.tar.gz" 21 | } 22 | }, 23 | "hash": { 24 | "url": "$url.sha256" 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to this project will be documented in this file. 3 | 4 | ## [0.3.0] - In progress 5 | 6 | ### Added 7 | - New commands added 8 | + Export activity 9 | + Export Movies 10 | + Search Media 11 | - Added api key support 12 | + All existing instances will be migrated on next execution 13 | 14 | ### Fixed 15 | - Dependency updates 16 | - Issue with executing tasks while background tasks were running 17 | - Documentation clarity 18 | 19 | ## [0.2.0] - 2022-09-12 20 | 21 | ### Added 22 | - Project repository structure corrections 23 | + Updated README.md 24 | + Added .gitignore 25 | + Added CHANGELOG.md 26 | - New commands added 27 | + Jellyfin restart 28 | + Jellyfin shutdown 29 | + Library information display 30 | + Plugin information display 31 | + Added ability to list tasks 32 | + Added ability to execute tasks by name 33 | + Added ability to export all user information excluding passwords 34 | + Added ability to import users from a file. 35 | 36 | ### Changed 37 | - Output displays 38 | - Project code structure reworked 39 | - Small changes to implement best practices 40 | 41 | ### Fixed 42 | - Fixed issue where some user details were being reverted to default values upon a policy update 43 | 44 | ## [0.1.0] - 2022-08-23 45 | Initial release 46 | -------------------------------------------------------------------------------- /src/entities/activity_details.rs: -------------------------------------------------------------------------------- 1 | use serde_derive::Deserialize; 2 | use serde_derive::Serialize; 3 | 4 | use comfy_table::{ContentArrangement, Table}; 5 | 6 | #[derive(Default, Clone, Serialize, Deserialize)] 7 | pub struct ActivityDetails { 8 | #[serde(rename = "Items")] 9 | pub items: Vec, 10 | #[serde(rename = "TotalRecordCount")] 11 | pub total_record_count: i64, 12 | #[serde(rename = "StartIndex")] 13 | pub start_index: i64, 14 | } 15 | 16 | #[derive(Default, Clone, Serialize, Deserialize)] 17 | pub struct Item { 18 | #[serde(rename = "Id")] 19 | pub id: i64, 20 | #[serde(rename = "Name")] 21 | pub name: String, 22 | #[serde(rename = "Overview", default)] 23 | pub overview: String, 24 | #[serde(rename = "ShortOverview", default)] 25 | pub short_overview: String, 26 | #[serde(rename = "Type")] 27 | pub type_field: String, 28 | #[serde(rename = "ItemId", default)] 29 | pub item_id: String, 30 | #[serde(rename = "Date")] 31 | pub date: String, 32 | #[serde(rename = "UserId")] 33 | pub user_id: String, 34 | #[serde(rename = "UserPrimaryImageTag", default)] 35 | pub user_primary_image_tag: String, 36 | #[serde(rename = "Severity")] 37 | pub severity: String, 38 | } 39 | 40 | impl ActivityDetails { 41 | pub fn table_print(activities: ActivityDetails) { 42 | let mut table = Table::new(); 43 | table 44 | .set_content_arrangement(ContentArrangement::Dynamic) 45 | .set_header(vec![ 46 | "Date", 47 | "User", 48 | "Type", 49 | "Severity", 50 | "Name", 51 | "ShortOverview", 52 | "Overview", 53 | ]); 54 | for activity in activities.items { 55 | table.add_row(vec![ 56 | &activity.date, 57 | &activity.id.to_string(), 58 | &activity.type_field, 59 | &activity.severity, 60 | &activity.name, 61 | &activity.short_overview, 62 | &activity.overview, 63 | ]); 64 | } 65 | println!("{table}"); 66 | } 67 | 68 | pub fn print_as_csv(activities: ActivityDetails) -> String { 69 | // first print the headers 70 | let mut data: String = "Date,User,Type,Severity,Name,ShortOverview,Overview\n".to_owned(); 71 | for activity in activities.items { 72 | let piece = format!( 73 | "{},{},{},{},{},{},{}\n", 74 | &activity.date, 75 | &activity.id.to_string(), 76 | &activity.type_field, 77 | &activity.severity, 78 | &activity.name, 79 | &activity.short_overview, 80 | &activity.overview 81 | ); 82 | data.push_str(&piece); 83 | } 84 | data 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/entities/device_details.rs: -------------------------------------------------------------------------------- 1 | use comfy_table::{ContentArrangement, Table}; 2 | 3 | #[derive(Serialize, Deserialize)] 4 | pub struct DeviceRootJson { 5 | #[serde(rename = "Items")] 6 | pub items: Vec, 7 | } 8 | 9 | #[derive(Serialize, Deserialize)] 10 | pub struct DeviceDetails { 11 | #[serde(rename = "Id")] 12 | pub id: String, 13 | #[serde(rename = "Name")] 14 | pub name: String, 15 | #[serde(rename = "LastUserName")] 16 | pub lastusername: String, 17 | #[serde(rename = "DateLastActivity")] 18 | pub lastactivity: String, 19 | } 20 | 21 | impl DeviceDetails { 22 | pub fn new( 23 | id: String, 24 | name: String, 25 | lastusername: String, 26 | lastactivity: String, 27 | ) -> DeviceDetails { 28 | DeviceDetails { 29 | id, 30 | name, 31 | lastusername, 32 | lastactivity, 33 | } 34 | } 35 | 36 | pub fn csv_print(devices: &[DeviceDetails]) { 37 | for device in devices { 38 | println!("{}, {}, {}", &device.id, &device.name, &device.lastusername); 39 | } 40 | } 41 | 42 | pub fn json_print(devices: &[DeviceDetails]) { 43 | println!("{}", serde_json::to_string_pretty(&devices).unwrap()); 44 | } 45 | 46 | pub fn table_print(devices: Vec) { 47 | let mut table = Table::new(); 48 | table 49 | .set_content_arrangement(ContentArrangement::Dynamic) 50 | .set_width(120) 51 | .set_header(vec!["Device Id", "Device Name", "Last Used By"]); 52 | for device in devices { 53 | table.add_row(vec![&device.id, &device.name, &device.lastusername]); 54 | } 55 | println!("{table}"); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/entities/library_details.rs: -------------------------------------------------------------------------------- 1 | use comfy_table::{ContentArrangement, Table}; 2 | 3 | pub type LibraryRootJson = Vec; 4 | 5 | #[derive(Serialize, Deserialize)] 6 | pub struct LibraryDetails { 7 | #[serde(rename = "Name")] 8 | pub name: String, 9 | #[serde(rename = "CollectionType")] 10 | pub collection_type: String, 11 | #[serde(rename = "ItemId")] 12 | pub item_id: String, 13 | #[serde(rename = "RefreshStatus")] 14 | pub refresh_status: String, 15 | } 16 | 17 | impl LibraryDetails { 18 | pub fn new( 19 | name: String, 20 | collection_type: String, 21 | item_id: String, 22 | refresh_status: String, 23 | ) -> LibraryDetails { 24 | LibraryDetails { 25 | name, 26 | collection_type, 27 | item_id, 28 | refresh_status, 29 | } 30 | } 31 | 32 | pub fn csv_print(libraries: Vec) { 33 | for library in libraries { 34 | println!("{}, {}, {}, {}", library.name, library.collection_type, library.item_id, library.refresh_status); 35 | } 36 | } 37 | 38 | pub fn json_print(libraries: &[LibraryDetails]) { 39 | println!("{}", serde_json::to_string_pretty(&libraries).unwrap()); 40 | } 41 | 42 | pub fn table_print(libraries: Vec) { 43 | let mut table = Table::new(); 44 | table 45 | .set_content_arrangement(ContentArrangement::Dynamic) 46 | .set_width(120) 47 | .set_header(vec![ 48 | "Library Name", 49 | "Collection Type", 50 | "Library Id", 51 | "Refresh Status", 52 | ]); 53 | for library in libraries { 54 | table.add_row(vec![ 55 | library.name, 56 | library.collection_type, 57 | library.item_id, 58 | library.refresh_status, 59 | ]); 60 | } 61 | println!("{table}"); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/entities/log_details.rs: -------------------------------------------------------------------------------- 1 | use comfy_table::{ContentArrangement, Table}; 2 | 3 | #[derive(Serialize, Deserialize)] 4 | pub struct LogDetails { 5 | #[serde(rename = "DateCreated")] 6 | pub date_created: String, 7 | #[serde(rename = "DateModified")] 8 | pub date_modified: String, 9 | #[serde(rename = "Name")] 10 | pub name: String, 11 | #[serde(rename = "Size")] 12 | pub size: i32, 13 | } 14 | 15 | impl LogDetails { 16 | pub fn new(date_created: String, date_modified: String, name: String, size: i32) -> LogDetails { 17 | LogDetails { 18 | date_created, 19 | date_modified, 20 | name, 21 | size, 22 | } 23 | } 24 | 25 | pub fn csv_print(logs: Vec) { 26 | for log in logs { 27 | println!("{}, {}, {}, {}", 28 | log.name, 29 | log.size, 30 | log.date_created, 31 | log.date_modified, 32 | ) 33 | } 34 | } 35 | 36 | pub fn json_print(logs: &[LogDetails]) { 37 | println!("{}", serde_json::to_string_pretty(&logs).unwrap()); 38 | } 39 | 40 | pub fn table_print(logs: Vec) { 41 | let mut table = Table::new(); 42 | table 43 | .set_content_arrangement(ContentArrangement::Dynamic) 44 | .set_width(120) 45 | .set_header(vec!["Log Name", "Size", "Date Created", "Last Modified"]); 46 | for log in logs { 47 | table.add_row(vec![ 48 | log.name, 49 | log.size.to_string(), 50 | log.date_created, 51 | log.date_modified, 52 | ]); 53 | } 54 | println!("{table}"); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/entities/media_details.rs: -------------------------------------------------------------------------------- 1 | // THIS IS CURRENTLY NOT USEABLE 2 | 3 | use comfy_table::{ContentArrangement, Table}; 4 | use serde_derive::Deserialize; 5 | use serde_derive::Serialize; 6 | 7 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 8 | #[serde(rename_all = "camelCase", default)] 9 | pub struct MediaRoot { 10 | #[serde(rename = "Items")] 11 | pub items: Vec, 12 | #[serde(rename = "TotalRecordCount")] 13 | pub total_record_count: i64, 14 | #[serde(rename = "StartIndex")] 15 | pub start_index: i64, 16 | } 17 | 18 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 19 | #[serde(rename_all = "camelCase", default)] 20 | pub struct MediaItem { 21 | #[serde(rename = "Name")] 22 | pub name: String, 23 | #[serde(rename = "OriginalTitle")] 24 | pub original_title: String, 25 | #[serde(rename = "ServerId")] 26 | pub server_id: String, 27 | 28 | #[serde(rename = "Id")] 29 | pub id: String, 30 | #[serde(rename = "Etag")] 31 | pub etag: String, 32 | #[serde(rename = "SourceType")] 33 | pub source_type: String, 34 | #[serde(rename = "PlaylistItemId")] 35 | pub playlist_item_id: String, 36 | #[serde(rename = "DateCreated")] 37 | pub date_created: String, 38 | #[serde(rename = "DateLastMediaAdded")] 39 | pub date_last_media_added: String, 40 | #[serde(rename = "ExtraType")] 41 | pub extra_type: String, 42 | #[serde(rename = "AirsBeforeSeasonNumber")] 43 | pub airs_before_season_number: i64, 44 | #[serde(rename = "AirsAfterSeasonNumber")] 45 | pub airs_after_season_number: i64, 46 | #[serde(rename = "AirsBeforeEpisodeNumber")] 47 | pub airs_before_episode_number: i64, 48 | #[serde(rename = "CanDelete")] 49 | pub can_delete: bool, 50 | #[serde(rename = "CanDownload")] 51 | pub can_download: bool, 52 | #[serde(rename = "HasSubtitles")] 53 | pub has_subtitles: bool, 54 | #[serde(rename = "PreferredMetadataLanguage")] 55 | pub preferred_metadata_language: String, 56 | #[serde(rename = "PreferredMetadataCountryCode")] 57 | pub preferred_metadata_country_code: String, 58 | #[serde(rename = "SupportsSync")] 59 | pub supports_sync: bool, 60 | #[serde(rename = "Container")] 61 | pub container: String, 62 | #[serde(rename = "SortName")] 63 | pub sort_name: String, 64 | #[serde(rename = "ForcedSortName")] 65 | pub forced_sort_name: String, 66 | #[serde(rename = "Video3DFormat")] 67 | pub video3dformat: String, 68 | #[serde(rename = "PremiereDate")] 69 | pub premiere_date: String, 70 | #[serde(rename = "ExternalUrls")] 71 | pub external_urls: Vec, 72 | #[serde(rename = "MediaSources")] 73 | pub media_sources: Vec, 74 | #[serde(rename = "CriticRating")] 75 | pub critic_rating: i64, 76 | #[serde(rename = "ProductionLocations")] 77 | pub production_locations: Vec, 78 | #[serde(rename = "Path")] 79 | pub path: String, 80 | #[serde(rename = "EnableMediaSourceDisplay")] 81 | pub enable_media_source_display: bool, 82 | #[serde(rename = "OfficialRating")] 83 | pub official_rating: String, 84 | #[serde(rename = "CustomRating")] 85 | pub custom_rating: String, 86 | #[serde(rename = "ChannelId", default)] 87 | pub channel_id: Option, 88 | #[serde(rename = "ChannelName")] 89 | pub channel_name: String, 90 | #[serde(rename = "Overview")] 91 | pub overview: String, 92 | #[serde(rename = "Taglines")] 93 | pub taglines: Vec, 94 | #[serde(rename = "Genres")] 95 | pub genres: Vec, 96 | #[serde(rename = "CommunityRating")] 97 | pub community_rating: f32, 98 | #[serde(rename = "CumulativeRunTimeTicks")] 99 | pub cumulative_run_time_ticks: i64, 100 | #[serde(rename = "RunTimeTicks")] 101 | pub run_time_ticks: i64, 102 | #[serde(rename = "PlayAccess")] 103 | pub play_access: String, 104 | #[serde(rename = "AspectRatio")] 105 | pub aspect_ratio: String, 106 | #[serde(rename = "ProductionYear")] 107 | pub production_year: i64, 108 | #[serde(rename = "IsPlaceHolder")] 109 | pub is_place_holder: bool, 110 | #[serde(rename = "Number")] 111 | pub number: String, 112 | #[serde(rename = "ChannelNumber")] 113 | pub channel_number: String, 114 | #[serde(rename = "IndexNumber")] 115 | pub index_number: i64, 116 | #[serde(rename = "IndexNumberEnd")] 117 | pub index_number_end: i64, 118 | #[serde(rename = "ParentIndexNumber")] 119 | pub parent_index_number: i64, 120 | #[serde(rename = "RemoteTrailers")] 121 | pub remote_trailers: Vec, 122 | #[serde(rename = "ProviderIds")] 123 | pub provider_ids: ProviderIds, 124 | #[serde(rename = "IsHD")] 125 | pub is_hd: bool, 126 | #[serde(rename = "IsFolder")] 127 | pub is_folder: bool, 128 | #[serde(rename = "ParentId")] 129 | pub parent_id: String, 130 | #[serde(rename = "Type")] 131 | pub type_field: String, 132 | #[serde(rename = "People")] 133 | pub people: Vec, 134 | #[serde(rename = "Studios")] 135 | pub studios: Vec, 136 | #[serde(rename = "GenreItems")] 137 | pub genre_items: Vec, 138 | #[serde(rename = "ParentLogoItemId")] 139 | pub parent_logo_item_id: String, 140 | #[serde(rename = "ParentBackdropItemId")] 141 | pub parent_backdrop_item_id: String, 142 | #[serde(rename = "ParentBackdropImageTags")] 143 | pub parent_backdrop_image_tags: Vec, 144 | #[serde(rename = "LocalTrailerCount")] 145 | pub local_trailer_count: i64, 146 | #[serde(rename = "UserData")] 147 | pub user_data: UserData, 148 | #[serde(rename = "RecursiveItemCount")] 149 | pub recursive_item_count: i64, 150 | #[serde(rename = "ChildCount")] 151 | pub child_count: i64, 152 | #[serde(rename = "SeriesName")] 153 | pub series_name: String, 154 | #[serde(rename = "SeriesId")] 155 | pub series_id: String, 156 | #[serde(rename = "SeasonId")] 157 | pub season_id: Option, 158 | #[serde(rename = "SpecialFeatureCount")] 159 | pub special_feature_count: i64, 160 | #[serde(rename = "DisplayPreferencesId")] 161 | pub display_preferences_id: String, 162 | #[serde(rename = "Status")] 163 | pub status: String, 164 | #[serde(rename = "AirTime")] 165 | pub air_time: String, 166 | #[serde(rename = "AirDays")] 167 | pub air_days: Vec, 168 | #[serde(rename = "Tags")] 169 | pub tags: Vec, 170 | #[serde(rename = "PrimaryImageAspectRatio")] 171 | pub primary_image_aspect_ratio: i64, 172 | #[serde(rename = "Artists")] 173 | pub artists: Vec, 174 | #[serde(rename = "ArtistItems")] 175 | pub artist_items: Vec, 176 | #[serde(rename = "Album")] 177 | pub album: String, 178 | #[serde(rename = "CollectionType")] 179 | pub collection_type: String, 180 | #[serde(rename = "DisplayOrder")] 181 | pub display_order: String, 182 | #[serde(rename = "AlbumId")] 183 | pub album_id: String, 184 | #[serde(rename = "AlbumPrimaryImageTag")] 185 | pub album_primary_image_tag: String, 186 | #[serde(rename = "SeriesPrimaryImageTag")] 187 | pub series_primary_image_tag: String, 188 | #[serde(rename = "AlbumArtist")] 189 | pub album_artist: String, 190 | #[serde(rename = "AlbumArtists")] 191 | pub album_artists: Vec, 192 | #[serde(rename = "SeasonName")] 193 | pub season_name: String, 194 | #[serde(rename = "MediaStreams")] 195 | pub media_streams: Vec, 196 | #[serde(rename = "VideoType")] 197 | pub video_type: String, 198 | #[serde(rename = "PartCount")] 199 | pub part_count: i64, 200 | #[serde(rename = "MediaSourceCount")] 201 | pub media_source_count: i64, 202 | #[serde(rename = "ImageTags")] 203 | pub image_tags: ImageTags, 204 | #[serde(rename = "BackdropImageTags")] 205 | pub backdrop_image_tags: Vec, 206 | #[serde(rename = "ScreenshotImageTags")] 207 | pub screenshot_image_tags: Vec, 208 | #[serde(rename = "ParentLogoImageTag")] 209 | pub parent_logo_image_tag: String, 210 | #[serde(rename = "ParentArtItemId")] 211 | pub parent_art_item_id: String, 212 | #[serde(rename = "ParentArtImageTag")] 213 | pub parent_art_image_tag: String, 214 | #[serde(rename = "SeriesThumbImageTag")] 215 | pub series_thumb_image_tag: String, 216 | #[serde(rename = "ImageBlurHashes")] 217 | pub image_blur_hashes: ImageBlurHashes2, 218 | #[serde(rename = "SeriesStudio")] 219 | pub series_studio: String, 220 | #[serde(rename = "ParentThumbItemId")] 221 | pub parent_thumb_item_id: String, 222 | #[serde(rename = "ParentThumbImageTag")] 223 | pub parent_thumb_image_tag: String, 224 | #[serde(rename = "ParentPrimaryImageItemId")] 225 | pub parent_primary_image_item_id: String, 226 | #[serde(rename = "ParentPrimaryImageTag")] 227 | pub parent_primary_image_tag: String, 228 | #[serde(rename = "Chapters")] 229 | pub chapters: Vec, 230 | #[serde(rename = "LocationType")] 231 | pub location_type: String, 232 | #[serde(rename = "IsoType")] 233 | pub iso_type: String, 234 | #[serde(rename = "MediaType")] 235 | pub media_type: String, 236 | #[serde(rename = "EndDate")] 237 | pub end_date: String, 238 | #[serde(rename = "LockedFields")] 239 | pub locked_fields: Vec, 240 | #[serde(rename = "TrailerCount")] 241 | pub trailer_count: i64, 242 | #[serde(rename = "MovieCount")] 243 | pub movie_count: i64, 244 | #[serde(rename = "SeriesCount")] 245 | pub series_count: i64, 246 | #[serde(rename = "ProgramCount")] 247 | pub program_count: i64, 248 | #[serde(rename = "EpisodeCount")] 249 | pub episode_count: i64, 250 | #[serde(rename = "SongCount")] 251 | pub song_count: i64, 252 | #[serde(rename = "AlbumCount")] 253 | pub album_count: i64, 254 | #[serde(rename = "ArtistCount")] 255 | pub artist_count: i64, 256 | #[serde(rename = "MusicVideoCount")] 257 | pub music_video_count: i64, 258 | #[serde(rename = "LockData")] 259 | pub lock_data: bool, 260 | #[serde(rename = "Width")] 261 | pub width: i64, 262 | #[serde(rename = "Height")] 263 | pub height: i64, 264 | #[serde(rename = "CameraMake")] 265 | pub camera_make: String, 266 | #[serde(rename = "CameraModel")] 267 | pub camera_model: String, 268 | #[serde(rename = "Software")] 269 | pub software: String, 270 | #[serde(rename = "ExposureTime")] 271 | pub exposure_time: i64, 272 | #[serde(rename = "FocalLength")] 273 | pub focal_length: i64, 274 | #[serde(rename = "ImageOrientation")] 275 | pub image_orientation: String, 276 | #[serde(rename = "Aperture")] 277 | pub aperture: i64, 278 | #[serde(rename = "ShutterSpeed")] 279 | pub shutter_speed: i64, 280 | #[serde(rename = "Latitude")] 281 | pub latitude: i64, 282 | #[serde(rename = "Longitude")] 283 | pub longitude: i64, 284 | #[serde(rename = "Altitude")] 285 | pub altitude: i64, 286 | #[serde(rename = "IsoSpeedRating")] 287 | pub iso_speed_rating: i64, 288 | #[serde(rename = "SeriesTimerId")] 289 | pub series_timer_id: String, 290 | #[serde(rename = "ProgramId")] 291 | pub program_id: String, 292 | #[serde(rename = "ChannelPrimaryImageTag")] 293 | pub channel_primary_image_tag: String, 294 | #[serde(rename = "StartDate")] 295 | pub start_date: String, 296 | #[serde(rename = "CompletionPercentage")] 297 | pub completion_percentage: i64, 298 | #[serde(rename = "IsRepeat")] 299 | pub is_repeat: bool, 300 | #[serde(rename = "EpisodeTitle")] 301 | pub episode_title: String, 302 | #[serde(rename = "ChannelType")] 303 | pub channel_type: String, 304 | #[serde(rename = "Audio")] 305 | pub audio: String, 306 | #[serde(rename = "IsMovie")] 307 | pub is_movie: bool, 308 | #[serde(rename = "IsSports")] 309 | pub is_sports: bool, 310 | #[serde(rename = "IsSeries")] 311 | pub is_series: bool, 312 | #[serde(rename = "IsLive")] 313 | pub is_live: bool, 314 | #[serde(rename = "IsNews")] 315 | pub is_news: bool, 316 | #[serde(rename = "IsKids")] 317 | pub is_kids: bool, 318 | #[serde(rename = "IsPremiere")] 319 | pub is_premiere: bool, 320 | #[serde(rename = "TimerId")] 321 | pub timer_id: String, 322 | #[serde(rename = "CurrentProgram")] 323 | pub current_program: CurrentProgram, 324 | } 325 | 326 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 327 | #[serde(rename_all = "camelCase", default)] 328 | pub struct ExternalUrl { 329 | #[serde(rename = "Name")] 330 | pub name: String, 331 | #[serde(rename = "Url")] 332 | pub url: String, 333 | } 334 | 335 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 336 | #[serde(rename_all = "camelCase", default)] 337 | pub struct MediaSource { 338 | #[serde(rename = "Protocol")] 339 | pub protocol: String, 340 | #[serde(rename = "Id")] 341 | pub id: String, 342 | #[serde(rename = "Path")] 343 | pub path: String, 344 | #[serde(rename = "EncoderPath")] 345 | pub encoder_path: String, 346 | #[serde(rename = "EncoderProtocol")] 347 | pub encoder_protocol: String, 348 | #[serde(rename = "Type")] 349 | pub type_field: String, 350 | #[serde(rename = "Container")] 351 | pub container: String, 352 | #[serde(rename = "Size")] 353 | pub size: i64, 354 | #[serde(rename = "Name")] 355 | pub name: String, 356 | #[serde(rename = "IsRemote")] 357 | pub is_remote: bool, 358 | #[serde(rename = "ETag")] 359 | pub etag: String, 360 | #[serde(rename = "RunTimeTicks")] 361 | pub run_time_ticks: i64, 362 | #[serde(rename = "ReadAtNativeFramerate")] 363 | pub read_at_native_framerate: bool, 364 | #[serde(rename = "IgnoreDts")] 365 | pub ignore_dts: bool, 366 | #[serde(rename = "IgnoreIndex")] 367 | pub ignore_index: bool, 368 | #[serde(rename = "GenPtsInput")] 369 | pub gen_pts_input: bool, 370 | #[serde(rename = "SupportsTranscoding")] 371 | pub supports_transcoding: bool, 372 | #[serde(rename = "SupportsDirectStream")] 373 | pub supports_direct_stream: bool, 374 | #[serde(rename = "SupportsDirectPlay")] 375 | pub supports_direct_play: bool, 376 | #[serde(rename = "IsInfiniteStream")] 377 | pub is_infinite_stream: bool, 378 | #[serde(rename = "RequiresOpening")] 379 | pub requires_opening: bool, 380 | #[serde(rename = "OpenToken")] 381 | pub open_token: String, 382 | #[serde(rename = "RequiresClosing")] 383 | pub requires_closing: bool, 384 | #[serde(rename = "LiveStreamId")] 385 | pub live_stream_id: String, 386 | #[serde(rename = "BufferMs")] 387 | pub buffer_ms: i64, 388 | #[serde(rename = "RequiresLooping")] 389 | pub requires_looping: bool, 390 | #[serde(rename = "SupportsProbing")] 391 | pub supports_probing: bool, 392 | #[serde(rename = "VideoType")] 393 | pub video_type: String, 394 | #[serde(rename = "IsoType")] 395 | pub iso_type: String, 396 | #[serde(rename = "Video3DFormat")] 397 | pub video3dformat: String, 398 | #[serde(rename = "MediaStreams")] 399 | pub media_streams: Vec, 400 | #[serde(rename = "MediaAttachments")] 401 | pub media_attachments: Vec, 402 | #[serde(rename = "Formats")] 403 | pub formats: Vec, 404 | #[serde(rename = "Bitrate")] 405 | pub bitrate: i64, 406 | #[serde(rename = "Timestamp")] 407 | pub timestamp: String, 408 | #[serde(rename = "RequiredHttpHeaders")] 409 | pub required_http_headers: RequiredHttpHeaders, 410 | #[serde(rename = "TranscodingUrl")] 411 | pub transcoding_url: String, 412 | #[serde(rename = "TranscodingSubProtocol")] 413 | pub transcoding_sub_protocol: String, 414 | #[serde(rename = "TranscodingContainer")] 415 | pub transcoding_container: String, 416 | #[serde(rename = "AnalyzeDurationMs")] 417 | pub analyze_duration_ms: i64, 418 | #[serde(rename = "DefaultAudioStreamIndex")] 419 | pub default_audio_stream_index: i64, 420 | #[serde(rename = "DefaultSubtitleStreamIndex")] 421 | pub default_subtitle_stream_index: i64, 422 | } 423 | 424 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 425 | #[serde(rename_all = "camelCase", default)] 426 | pub struct MediaStream { 427 | #[serde(rename = "Codec")] 428 | pub codec: String, 429 | #[serde(rename = "CodecTag")] 430 | pub codec_tag: String, 431 | #[serde(rename = "Language")] 432 | pub language: String, 433 | #[serde(rename = "ColorRange")] 434 | pub color_range: String, 435 | #[serde(rename = "ColorSpace")] 436 | pub color_space: String, 437 | #[serde(rename = "ColorTransfer")] 438 | pub color_transfer: String, 439 | #[serde(rename = "ColorPrimaries")] 440 | pub color_primaries: String, 441 | #[serde(rename = "DvVersionMajor")] 442 | pub dv_version_major: i64, 443 | #[serde(rename = "DvVersionMinor")] 444 | pub dv_version_minor: i64, 445 | #[serde(rename = "DvProfile")] 446 | pub dv_profile: i64, 447 | #[serde(rename = "DvLevel")] 448 | pub dv_level: i64, 449 | #[serde(rename = "RpuPresentFlag")] 450 | pub rpu_present_flag: i64, 451 | #[serde(rename = "ElPresentFlag")] 452 | pub el_present_flag: i64, 453 | #[serde(rename = "BlPresentFlag")] 454 | pub bl_present_flag: i64, 455 | #[serde(rename = "DvBlSignalCompatibilityId")] 456 | pub dv_bl_signal_compatibility_id: i64, 457 | #[serde(rename = "Comment")] 458 | pub comment: String, 459 | #[serde(rename = "TimeBase")] 460 | pub time_base: String, 461 | #[serde(rename = "CodecTimeBase")] 462 | pub codec_time_base: String, 463 | #[serde(rename = "Title")] 464 | pub title: String, 465 | #[serde(rename = "VideoRange")] 466 | pub video_range: String, 467 | #[serde(rename = "VideoRangeType")] 468 | pub video_range_type: String, 469 | #[serde(rename = "VideoDoViTitle")] 470 | pub video_do_vi_title: String, 471 | #[serde(rename = "LocalizedUndefined")] 472 | pub localized_undefined: String, 473 | #[serde(rename = "LocalizedDefault")] 474 | pub localized_default: String, 475 | #[serde(rename = "LocalizedForced")] 476 | pub localized_forced: String, 477 | #[serde(rename = "LocalizedExternal")] 478 | pub localized_external: String, 479 | #[serde(rename = "DisplayTitle")] 480 | pub display_title: String, 481 | #[serde(rename = "NalLengthSize")] 482 | pub nal_length_size: String, 483 | #[serde(rename = "IsInterlaced")] 484 | pub is_interlaced: bool, 485 | #[serde(rename = "IsAVC")] 486 | pub is_avc: bool, 487 | #[serde(rename = "ChannelLayout")] 488 | pub channel_layout: String, 489 | #[serde(rename = "BitRate")] 490 | pub bit_rate: i64, 491 | #[serde(rename = "BitDepth")] 492 | pub bit_depth: i64, 493 | #[serde(rename = "RefFrames")] 494 | pub ref_frames: i64, 495 | #[serde(rename = "PacketLength")] 496 | pub packet_length: i64, 497 | #[serde(rename = "Channels")] 498 | pub channels: i64, 499 | #[serde(rename = "SampleRate")] 500 | pub sample_rate: i64, 501 | #[serde(rename = "IsDefault")] 502 | pub is_default: bool, 503 | #[serde(rename = "IsForced")] 504 | pub is_forced: bool, 505 | #[serde(rename = "Height")] 506 | pub height: i64, 507 | #[serde(rename = "Width")] 508 | pub width: i64, 509 | #[serde(rename = "AverageFrameRate")] 510 | pub average_frame_rate: i64, 511 | #[serde(rename = "RealFrameRate")] 512 | pub real_frame_rate: i64, 513 | #[serde(rename = "Profile")] 514 | pub profile: String, 515 | #[serde(rename = "Type")] 516 | pub type_field: String, 517 | #[serde(rename = "AspectRatio")] 518 | pub aspect_ratio: String, 519 | #[serde(rename = "Index")] 520 | pub index: i64, 521 | #[serde(rename = "Score")] 522 | pub score: i64, 523 | #[serde(rename = "IsExternal")] 524 | pub is_external: bool, 525 | #[serde(rename = "DeliveryMethod")] 526 | pub delivery_method: String, 527 | #[serde(rename = "DeliveryUrl")] 528 | pub delivery_url: String, 529 | #[serde(rename = "IsExternalUrl")] 530 | pub is_external_url: bool, 531 | #[serde(rename = "IsTextSubtitleStream")] 532 | pub is_text_subtitle_stream: bool, 533 | #[serde(rename = "SupportsExternalStream")] 534 | pub supports_external_stream: bool, 535 | #[serde(rename = "Path")] 536 | pub path: String, 537 | #[serde(rename = "PixelFormat")] 538 | pub pixel_format: String, 539 | #[serde(rename = "Level")] 540 | pub level: i64, 541 | #[serde(rename = "IsAnamorphic")] 542 | pub is_anamorphic: bool, 543 | } 544 | 545 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 546 | #[serde(rename_all = "camelCase", default)] 547 | pub struct MediaAttachment { 548 | #[serde(rename = "Codec")] 549 | pub codec: String, 550 | #[serde(rename = "CodecTag")] 551 | pub codec_tag: String, 552 | #[serde(rename = "Comment")] 553 | pub comment: String, 554 | #[serde(rename = "Index")] 555 | pub index: i64, 556 | #[serde(rename = "FileName")] 557 | pub file_name: String, 558 | #[serde(rename = "MimeType")] 559 | pub mime_type: String, 560 | #[serde(rename = "DeliveryUrl")] 561 | pub delivery_url: String, 562 | } 563 | 564 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 565 | #[serde(rename_all = "camelCase", default)] 566 | pub struct RequiredHttpHeaders { 567 | pub property1: String, 568 | pub property2: String, 569 | } 570 | 571 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 572 | #[serde(rename_all = "camelCase", default)] 573 | pub struct RemoteTrailer { 574 | #[serde(rename = "Url")] 575 | pub url: String, 576 | #[serde(rename = "Name")] 577 | pub name: String, 578 | } 579 | 580 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 581 | #[serde(rename_all = "camelCase", default)] 582 | pub struct ProviderIds { 583 | pub property1: String, 584 | pub property2: String, 585 | } 586 | 587 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 588 | #[serde(rename_all = "camelCase", default)] 589 | pub struct People { 590 | #[serde(rename = "Name")] 591 | pub name: String, 592 | #[serde(rename = "Id")] 593 | pub id: String, 594 | #[serde(rename = "Role")] 595 | pub role: String, 596 | #[serde(rename = "Type")] 597 | pub type_field: String, 598 | #[serde(rename = "PrimaryImageTag")] 599 | pub primary_image_tag: String, 600 | #[serde(rename = "ImageBlurHashes")] 601 | pub image_blur_hashes: ImageBlurHashes, 602 | } 603 | 604 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 605 | #[serde(rename_all = "camelCase", default)] 606 | pub struct ImageBlurHashes { 607 | #[serde(rename = "Primary")] 608 | pub primary: Primary, 609 | #[serde(rename = "Art")] 610 | pub art: Art, 611 | #[serde(rename = "Backdrop")] 612 | pub backdrop: Backdrop, 613 | #[serde(rename = "Banner")] 614 | pub banner: Banner, 615 | #[serde(rename = "Logo")] 616 | pub logo: Logo, 617 | #[serde(rename = "Thumb")] 618 | pub thumb: Thumb, 619 | #[serde(rename = "Disc")] 620 | pub disc: Disc, 621 | #[serde(rename = "Box")] 622 | pub box_field: Box, 623 | #[serde(rename = "Screenshot")] 624 | pub screenshot: Screenshot, 625 | #[serde(rename = "Menu")] 626 | pub menu: Menu, 627 | #[serde(rename = "Chapter")] 628 | pub chapter: Chapter, 629 | #[serde(rename = "BoxRear")] 630 | pub box_rear: BoxRear, 631 | #[serde(rename = "Profile")] 632 | pub profile: Profile, 633 | } 634 | 635 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 636 | #[serde(rename_all = "camelCase", default)] 637 | pub struct Primary { 638 | pub property1: String, 639 | pub property2: String, 640 | } 641 | 642 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 643 | #[serde(rename_all = "camelCase", default)] 644 | pub struct Art { 645 | pub property1: String, 646 | pub property2: String, 647 | } 648 | 649 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 650 | #[serde(rename_all = "camelCase", default)] 651 | pub struct Backdrop { 652 | pub property1: String, 653 | pub property2: String, 654 | } 655 | 656 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 657 | #[serde(rename_all = "camelCase", default)] 658 | pub struct Banner { 659 | pub property1: String, 660 | pub property2: String, 661 | } 662 | 663 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 664 | #[serde(rename_all = "camelCase", default)] 665 | pub struct Logo { 666 | pub property1: String, 667 | pub property2: String, 668 | } 669 | 670 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 671 | #[serde(rename_all = "camelCase", default)] 672 | pub struct Thumb { 673 | pub property1: String, 674 | pub property2: String, 675 | } 676 | 677 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 678 | #[serde(rename_all = "camelCase", default)] 679 | pub struct Disc { 680 | pub property1: String, 681 | pub property2: String, 682 | } 683 | 684 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 685 | #[serde(rename_all = "camelCase", default)] 686 | pub struct Box { 687 | pub property1: String, 688 | pub property2: String, 689 | } 690 | 691 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 692 | #[serde(rename_all = "camelCase", default)] 693 | pub struct Screenshot { 694 | pub property1: String, 695 | pub property2: String, 696 | } 697 | 698 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 699 | #[serde(rename_all = "camelCase", default)] 700 | pub struct Menu { 701 | pub property1: String, 702 | pub property2: String, 703 | } 704 | 705 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 706 | #[serde(rename_all = "camelCase", default)] 707 | pub struct Chapter { 708 | pub property1: String, 709 | pub property2: String, 710 | } 711 | 712 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 713 | #[serde(rename_all = "camelCase", default)] 714 | pub struct BoxRear { 715 | pub property1: String, 716 | pub property2: String, 717 | } 718 | 719 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 720 | #[serde(rename_all = "camelCase", default)] 721 | pub struct Profile { 722 | pub property1: String, 723 | pub property2: String, 724 | } 725 | 726 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 727 | #[serde(rename_all = "camelCase", default)] 728 | pub struct Studio { 729 | #[serde(rename = "Name")] 730 | pub name: String, 731 | #[serde(rename = "Id")] 732 | pub id: String, 733 | } 734 | 735 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 736 | #[serde(rename_all = "camelCase", default)] 737 | pub struct GenreItem { 738 | #[serde(rename = "Name")] 739 | pub name: String, 740 | #[serde(rename = "Id")] 741 | pub id: String, 742 | } 743 | 744 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 745 | #[serde(rename_all = "camelCase", default)] 746 | pub struct UserData { 747 | #[serde(rename = "Rating")] 748 | pub rating: i64, 749 | #[serde(rename = "PlayedPercentage")] 750 | pub played_percentage: i64, 751 | #[serde(rename = "UnplayedItemCount")] 752 | pub unplayed_item_count: i64, 753 | #[serde(rename = "PlaybackPositionTicks")] 754 | pub playback_position_ticks: i64, 755 | #[serde(rename = "PlayCount")] 756 | pub play_count: i64, 757 | #[serde(rename = "IsFavorite")] 758 | pub is_favorite: bool, 759 | #[serde(rename = "Likes")] 760 | pub likes: bool, 761 | #[serde(rename = "LastPlayedDate")] 762 | pub last_played_date: String, 763 | #[serde(rename = "Played")] 764 | pub played: bool, 765 | #[serde(rename = "Key")] 766 | pub key: String, 767 | #[serde(rename = "ItemId")] 768 | pub item_id: String, 769 | } 770 | 771 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 772 | #[serde(rename_all = "camelCase", default)] 773 | pub struct ArtistItem { 774 | #[serde(rename = "Name")] 775 | pub name: String, 776 | #[serde(rename = "Id")] 777 | pub id: String, 778 | } 779 | 780 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 781 | #[serde(rename_all = "camelCase", default)] 782 | pub struct AlbumArtist { 783 | #[serde(rename = "Name")] 784 | pub name: String, 785 | #[serde(rename = "Id")] 786 | pub id: String, 787 | } 788 | 789 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 790 | #[serde(rename_all = "camelCase", default)] 791 | pub struct MediaStream2 { 792 | #[serde(rename = "Codec")] 793 | pub codec: String, 794 | #[serde(rename = "CodecTag")] 795 | pub codec_tag: String, 796 | #[serde(rename = "Language")] 797 | pub language: String, 798 | #[serde(rename = "ColorRange")] 799 | pub color_range: String, 800 | #[serde(rename = "ColorSpace")] 801 | pub color_space: String, 802 | #[serde(rename = "ColorTransfer")] 803 | pub color_transfer: String, 804 | #[serde(rename = "ColorPrimaries")] 805 | pub color_primaries: String, 806 | #[serde(rename = "DvVersionMajor")] 807 | pub dv_version_major: i64, 808 | #[serde(rename = "DvVersionMinor")] 809 | pub dv_version_minor: i64, 810 | #[serde(rename = "DvProfile")] 811 | pub dv_profile: i64, 812 | #[serde(rename = "DvLevel")] 813 | pub dv_level: i64, 814 | #[serde(rename = "RpuPresentFlag")] 815 | pub rpu_present_flag: i64, 816 | #[serde(rename = "ElPresentFlag")] 817 | pub el_present_flag: i64, 818 | #[serde(rename = "BlPresentFlag")] 819 | pub bl_present_flag: i64, 820 | #[serde(rename = "DvBlSignalCompatibilityId")] 821 | pub dv_bl_signal_compatibility_id: i64, 822 | #[serde(rename = "Comment")] 823 | pub comment: String, 824 | #[serde(rename = "TimeBase")] 825 | pub time_base: String, 826 | #[serde(rename = "CodecTimeBase")] 827 | pub codec_time_base: String, 828 | #[serde(rename = "Title")] 829 | pub title: String, 830 | #[serde(rename = "VideoRange")] 831 | pub video_range: String, 832 | #[serde(rename = "VideoRangeType")] 833 | pub video_range_type: String, 834 | #[serde(rename = "VideoDoViTitle")] 835 | pub video_do_vi_title: String, 836 | #[serde(rename = "LocalizedUndefined")] 837 | pub localized_undefined: String, 838 | #[serde(rename = "LocalizedDefault")] 839 | pub localized_default: String, 840 | #[serde(rename = "LocalizedForced")] 841 | pub localized_forced: String, 842 | #[serde(rename = "LocalizedExternal")] 843 | pub localized_external: String, 844 | #[serde(rename = "DisplayTitle")] 845 | pub display_title: String, 846 | #[serde(rename = "NalLengthSize")] 847 | pub nal_length_size: String, 848 | #[serde(rename = "IsInterlaced")] 849 | pub is_interlaced: bool, 850 | #[serde(rename = "IsAVC")] 851 | pub is_avc: bool, 852 | #[serde(rename = "ChannelLayout")] 853 | pub channel_layout: String, 854 | #[serde(rename = "BitRate")] 855 | pub bit_rate: i64, 856 | #[serde(rename = "BitDepth")] 857 | pub bit_depth: i64, 858 | #[serde(rename = "RefFrames")] 859 | pub ref_frames: i64, 860 | #[serde(rename = "PacketLength")] 861 | pub packet_length: i64, 862 | #[serde(rename = "Channels")] 863 | pub channels: i64, 864 | #[serde(rename = "SampleRate")] 865 | pub sample_rate: i64, 866 | #[serde(rename = "IsDefault")] 867 | pub is_default: bool, 868 | #[serde(rename = "IsForced")] 869 | pub is_forced: bool, 870 | #[serde(rename = "Height")] 871 | pub height: i64, 872 | #[serde(rename = "Width")] 873 | pub width: i64, 874 | #[serde(rename = "AverageFrameRate")] 875 | pub average_frame_rate: i64, 876 | #[serde(rename = "RealFrameRate")] 877 | pub real_frame_rate: i64, 878 | #[serde(rename = "Profile")] 879 | pub profile: String, 880 | #[serde(rename = "Type")] 881 | pub type_field: String, 882 | #[serde(rename = "AspectRatio")] 883 | pub aspect_ratio: String, 884 | #[serde(rename = "Index")] 885 | pub index: i64, 886 | #[serde(rename = "Score")] 887 | pub score: i64, 888 | #[serde(rename = "IsExternal")] 889 | pub is_external: bool, 890 | #[serde(rename = "DeliveryMethod")] 891 | pub delivery_method: String, 892 | #[serde(rename = "DeliveryUrl")] 893 | pub delivery_url: String, 894 | #[serde(rename = "IsExternalUrl")] 895 | pub is_external_url: bool, 896 | #[serde(rename = "IsTextSubtitleStream")] 897 | pub is_text_subtitle_stream: bool, 898 | #[serde(rename = "SupportsExternalStream")] 899 | pub supports_external_stream: bool, 900 | #[serde(rename = "Path")] 901 | pub path: String, 902 | #[serde(rename = "PixelFormat")] 903 | pub pixel_format: String, 904 | #[serde(rename = "Level")] 905 | pub level: i64, 906 | #[serde(rename = "IsAnamorphic")] 907 | pub is_anamorphic: bool, 908 | } 909 | 910 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 911 | #[serde(rename_all = "camelCase", default)] 912 | pub struct ImageTags { 913 | pub property1: String, 914 | pub property2: String, 915 | } 916 | 917 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 918 | #[serde(rename_all = "camelCase", default)] 919 | pub struct ImageBlurHashes2 { 920 | #[serde(rename = "Primary")] 921 | pub primary: Primary2, 922 | #[serde(rename = "Art")] 923 | pub art: Art2, 924 | #[serde(rename = "Backdrop")] 925 | pub backdrop: Backdrop2, 926 | #[serde(rename = "Banner")] 927 | pub banner: Banner2, 928 | #[serde(rename = "Logo")] 929 | pub logo: Logo2, 930 | #[serde(rename = "Thumb")] 931 | pub thumb: Thumb2, 932 | #[serde(rename = "Disc")] 933 | pub disc: Disc2, 934 | #[serde(rename = "Box")] 935 | pub box_field: Box2, 936 | #[serde(rename = "Screenshot")] 937 | pub screenshot: Screenshot2, 938 | #[serde(rename = "Menu")] 939 | pub menu: Menu2, 940 | #[serde(rename = "Chapter")] 941 | pub chapter: Chapter2, 942 | #[serde(rename = "BoxRear")] 943 | pub box_rear: BoxRear2, 944 | #[serde(rename = "Profile")] 945 | pub profile: Profile2, 946 | } 947 | 948 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 949 | #[serde(rename_all = "camelCase", default)] 950 | pub struct Primary2 { 951 | pub property1: String, 952 | pub property2: String, 953 | } 954 | 955 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 956 | #[serde(rename_all = "camelCase", default)] 957 | pub struct Art2 { 958 | pub property1: String, 959 | pub property2: String, 960 | } 961 | 962 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 963 | #[serde(rename_all = "camelCase", default)] 964 | pub struct Backdrop2 { 965 | pub property1: String, 966 | pub property2: String, 967 | } 968 | 969 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 970 | #[serde(rename_all = "camelCase", default)] 971 | pub struct Banner2 { 972 | pub property1: String, 973 | pub property2: String, 974 | } 975 | 976 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 977 | #[serde(rename_all = "camelCase", default)] 978 | pub struct Logo2 { 979 | pub property1: String, 980 | pub property2: String, 981 | } 982 | 983 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 984 | #[serde(rename_all = "camelCase", default)] 985 | pub struct Thumb2 { 986 | pub property1: String, 987 | pub property2: String, 988 | } 989 | 990 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 991 | #[serde(rename_all = "camelCase", default)] 992 | pub struct Disc2 { 993 | pub property1: String, 994 | pub property2: String, 995 | } 996 | 997 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 998 | #[serde(rename_all = "camelCase", default)] 999 | pub struct Box2 { 1000 | pub property1: String, 1001 | pub property2: String, 1002 | } 1003 | 1004 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 1005 | #[serde(rename_all = "camelCase", default)] 1006 | pub struct Screenshot2 { 1007 | pub property1: String, 1008 | pub property2: String, 1009 | } 1010 | 1011 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 1012 | #[serde(rename_all = "camelCase", default)] 1013 | pub struct Menu2 { 1014 | pub property1: String, 1015 | pub property2: String, 1016 | } 1017 | 1018 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 1019 | #[serde(rename_all = "camelCase", default)] 1020 | pub struct Chapter2 { 1021 | pub property1: String, 1022 | pub property2: String, 1023 | } 1024 | 1025 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 1026 | #[serde(rename_all = "camelCase", default)] 1027 | pub struct BoxRear2 { 1028 | pub property1: String, 1029 | pub property2: String, 1030 | } 1031 | 1032 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 1033 | #[serde(rename_all = "camelCase", default)] 1034 | pub struct Profile2 { 1035 | pub property1: String, 1036 | pub property2: String, 1037 | } 1038 | 1039 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 1040 | #[serde(rename_all = "camelCase", default)] 1041 | pub struct Chapter3 { 1042 | #[serde(rename = "StartPositionTicks")] 1043 | pub start_position_ticks: i64, 1044 | #[serde(rename = "Name")] 1045 | pub name: String, 1046 | #[serde(rename = "ImagePath")] 1047 | pub image_path: String, 1048 | #[serde(rename = "ImageDateModified")] 1049 | pub image_date_modified: String, 1050 | #[serde(rename = "ImageTag")] 1051 | pub image_tag: String, 1052 | } 1053 | 1054 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 1055 | #[serde(rename_all = "camelCase", default)] 1056 | pub struct CurrentProgram {} 1057 | 1058 | impl MediaRoot { 1059 | pub fn table_print(media: MediaRoot, table_columns: &Vec) { 1060 | let mut table = Table::new(); 1061 | table 1062 | .set_content_arrangement(ContentArrangement::Dynamic) 1063 | .set_header(table_columns); 1064 | for media_item in media.items { 1065 | table.add_row(build_table_row(&media_item, table_columns)); 1066 | } 1067 | println!("{table}"); 1068 | } 1069 | 1070 | pub fn csv_print(media: MediaRoot, table_columns: &Vec) { 1071 | let mut wtr = csv::Writer::from_writer(std::io::stdout()); 1072 | 1073 | for media_item in media.items { 1074 | wtr.write_record(build_table_row(&media_item, table_columns)) 1075 | .unwrap(); 1076 | } 1077 | } 1078 | 1079 | pub fn json_print(media: MediaRoot) { 1080 | println!("{}", serde_json::to_string_pretty(&media).unwrap()); 1081 | } 1082 | } 1083 | 1084 | fn build_table_row(media_item: &MediaItem, table_columns: &Vec) -> Vec { 1085 | let mut row = Vec::new(); 1086 | 1087 | for column in table_columns { 1088 | match column.to_uppercase().as_str() { 1089 | "NAME" => row.push(media_item.name.to_string()), 1090 | "ID" => row.push(media_item.id.to_string()), 1091 | "TYPE" => row.push(media_item.type_field.to_string()), 1092 | "PATH" => row.push(media_item.path.to_string()), 1093 | "CRITICRATING" => row.push(media_item.critic_rating.to_string()), 1094 | "PRODUCTIONYEAR" => row.push(media_item.production_year.to_string()), 1095 | _ => row.push("?".to_string()), 1096 | } 1097 | } 1098 | 1099 | row 1100 | } 1101 | -------------------------------------------------------------------------------- /src/entities/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod activity_details; 2 | pub mod device_details; 3 | pub mod library_details; 4 | pub mod log_details; 5 | pub mod media_details; 6 | pub mod movie_details; 7 | pub mod package_details; 8 | pub mod plugin_details; 9 | pub mod repository_details; 10 | pub mod server_info; 11 | pub mod task_details; 12 | pub mod token_details; 13 | pub mod user_details; 14 | -------------------------------------------------------------------------------- /src/entities/movie_details.rs: -------------------------------------------------------------------------------- 1 | use comfy_table::{ContentArrangement, Table}; 2 | use serde_derive::Deserialize; 3 | use serde_derive::Serialize; 4 | 5 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 6 | #[serde(rename_all = "camelCase")] 7 | pub struct MovieDetails { 8 | #[serde(rename = "Items")] 9 | pub items: Vec, 10 | } 11 | 12 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 13 | #[serde(rename_all = "camelCase")] 14 | pub struct Item { 15 | #[serde(rename = "Name")] 16 | pub name: String, 17 | #[serde(rename = "DateCreated")] 18 | pub date_created: String, 19 | #[serde(rename = "HasSubtitles", default)] 20 | pub has_subtitles: bool, 21 | #[serde(rename = "PremiereDate", default)] 22 | pub premiere_date: String, 23 | #[serde(rename = "Path")] 24 | pub path: String, 25 | #[serde(rename = "OfficialRating", default)] 26 | pub official_rating: String, 27 | #[serde(rename = "Genres", default)] 28 | pub genres: Vec, 29 | #[serde(rename = "CommunityRating", default)] 30 | pub community_rating: f32, 31 | #[serde(rename = "RunTimeTicks", default)] 32 | pub run_time_ticks: i64, 33 | #[serde(rename = "ProductionYear", default)] 34 | pub production_year: i64, 35 | #[serde(rename = "Width", default)] 36 | pub width: i64, 37 | #[serde(rename = "Height", default)] 38 | pub height: i64, 39 | } 40 | 41 | impl MovieDetails { 42 | pub fn table_print(movies: MovieDetails) { 43 | let mut table = Table::new(); 44 | table 45 | .set_content_arrangement(ContentArrangement::Dynamic) 46 | .set_header(vec![ 47 | "Name", 48 | "Date Added", 49 | "Premiere Date", 50 | "Release Year", 51 | "Genres", 52 | "Parental Rating", 53 | "Community Rating", 54 | "Runtime (in minutes)", 55 | "Resolution", 56 | "Subtitles", 57 | "Path ", 58 | ]); 59 | for movie in movies.items { 60 | table.add_row(vec![ 61 | &movie.name, 62 | &movie.date_created, 63 | &movie.premiere_date, 64 | &movie.production_year.to_string(), 65 | &Self::genres_to_string(&movie), 66 | &movie.official_rating, 67 | &movie.community_rating.to_string(), 68 | &Self::ticks_to_minutes(&movie.run_time_ticks).to_string(), 69 | &Self::format_resolution(movie.width.to_string(), movie.height.to_string()), 70 | &movie.has_subtitles.to_string(), 71 | &movie.path, 72 | ]); 73 | } 74 | println!("{table}"); 75 | } 76 | 77 | pub fn print_as_csv(movies: MovieDetails) -> String { 78 | let mut data: String = "Name,Date Added,Premiere Date,Release Year,Genres,Parental Rating,Community Rating,Runtime (in minutes),Resolution,Subtitles,Path\n".to_owned(); 79 | for movie in movies.items { 80 | let piece = format!( 81 | "{},{},{},{},{},{},{},{},{},{},{}\n", 82 | movie.name, 83 | movie.date_created, 84 | movie.premiere_date, 85 | movie.production_year, 86 | Self::genres_to_string(&movie), 87 | movie.official_rating, 88 | movie.community_rating, 89 | Self::ticks_to_minutes(&movie.run_time_ticks), 90 | Self::format_resolution(movie.width.to_string(), movie.height.to_string()), 91 | movie.has_subtitles, 92 | movie.path 93 | ); 94 | data.push_str(&piece); 95 | } 96 | data 97 | } 98 | 99 | fn ticks_to_minutes(ticks: &i64) -> i64 { 100 | ticks / 10000000 / 60 101 | } 102 | 103 | fn genres_to_string(movie: &Item) -> String { 104 | let string = &movie 105 | .genres 106 | .iter() 107 | .map(|x| x.to_string() + ";") 108 | .collect::(); 109 | string.trim_end_matches(',').to_string() 110 | } 111 | 112 | fn format_resolution(width: String, height: String) -> String { 113 | format!("{} * {}", width, height) 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/entities/package_details.rs: -------------------------------------------------------------------------------- 1 | use comfy_table::{ContentArrangement, Table}; 2 | pub type PackageDetailsRoot = Vec; 3 | 4 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 5 | #[serde(rename_all = "camelCase")] 6 | pub struct PackageDetails { 7 | pub name: String, 8 | pub description: String, 9 | pub overview: String, 10 | pub owner: String, 11 | pub category: String, 12 | pub guid: String, 13 | pub versions: Vec, 14 | #[serde(default)] 15 | pub image_url: String, 16 | } 17 | 18 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 19 | #[serde(rename_all = "camelCase")] 20 | pub struct Version { 21 | pub version: String, 22 | #[serde(rename = "VersionNumber")] 23 | pub version_number: String, 24 | pub changelog: String, 25 | pub target_abi: String, 26 | pub source_url: String, 27 | pub checksum: String, 28 | pub timestamp: String, 29 | pub repository_name: String, 30 | pub repository_url: String, 31 | } 32 | 33 | impl PackageDetails { 34 | pub fn csv_print(packages: Vec) { 35 | for package in packages { 36 | let mut version_output: String = "".to_string(); 37 | for version in package.versions { 38 | version_output.push_str(version.version.as_str()); 39 | version_output.push(' '); 40 | } 41 | println!("{}, {}, {}, {}, {}, {}, {}", 42 | package.name, 43 | package.description, 44 | package.overview, 45 | package.owner, 46 | package.guid, 47 | package.category, 48 | version_output, 49 | ); 50 | } 51 | } 52 | 53 | pub fn json_print(packages: &[PackageDetails]) { 54 | println!("{}", serde_json::to_string_pretty(&packages).unwrap()); 55 | } 56 | 57 | pub fn table_print(packages: Vec) { 58 | let mut table = Table::new(); 59 | table 60 | .set_content_arrangement(ContentArrangement::Dynamic) 61 | .set_width(120) 62 | .set_header(vec![ 63 | "Name", 64 | "Description", 65 | "Overview", 66 | "Owner", 67 | "GUID", 68 | "Category", 69 | "Versions", 70 | ]); 71 | for package in packages { 72 | let mut version_output: String = "".to_string(); 73 | for version in package.versions { 74 | version_output.push_str(version.version.as_str()); 75 | version_output.push(' '); 76 | } 77 | table.add_row(vec![ 78 | package.name, 79 | package.description, 80 | package.overview, 81 | package.owner, 82 | package.guid, 83 | package.category, 84 | version_output, 85 | ]); 86 | } 87 | println!("{table}"); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/entities/plugin_details.rs: -------------------------------------------------------------------------------- 1 | use comfy_table::{ContentArrangement, Table}; 2 | 3 | pub type PluginRootJson = Vec; 4 | 5 | #[derive(Serialize, Deserialize)] 6 | pub struct PluginDetails { 7 | #[serde(rename = "Name")] 8 | pub name: String, 9 | #[serde(rename = "Version")] 10 | pub version: String, 11 | #[serde(rename = "ConfigurationFileName")] 12 | pub configuration_file_name: Option, 13 | #[serde(rename = "Description")] 14 | pub description: String, 15 | #[serde(rename = "Id")] 16 | pub id: String, 17 | #[serde(rename = "CanUninstall")] 18 | pub can_uninstall: bool, 19 | #[serde(rename = "HasImage")] 20 | pub has_image: bool, 21 | #[serde(rename = "Status")] 22 | pub status: String, 23 | } 24 | 25 | impl PluginDetails { 26 | pub fn csv_print(plugins: Vec) { 27 | for plugin in plugins { 28 | println!("{}, {}, {}, {}, {}, {}, {}, {}", 29 | plugin.name, 30 | plugin.version, 31 | plugin 32 | .configuration_file_name 33 | .unwrap_or_else(|| String::new()), 34 | plugin.description, 35 | plugin.id, 36 | plugin.can_uninstall, 37 | plugin.has_image, 38 | plugin.status, 39 | ) 40 | } 41 | } 42 | 43 | pub fn json_print(plugins: &[PluginDetails]) { 44 | println!("{}", serde_json::to_string_pretty(&plugins).unwrap()); 45 | } 46 | 47 | pub fn table_print(plugins: Vec) { 48 | let mut table = Table::new(); 49 | table 50 | .set_content_arrangement(ContentArrangement::Dynamic) 51 | .set_width(120) 52 | .set_header(vec![ 53 | "Plugin Name", 54 | "Version", 55 | "Config Filename", 56 | "Description", 57 | "Id", 58 | "Can Uninstall", 59 | "Image", 60 | "Status", 61 | ]); 62 | for plugin in plugins { 63 | table.add_row(vec![ 64 | plugin.name, 65 | plugin.version, 66 | plugin 67 | .configuration_file_name 68 | .unwrap_or_else(|| String::new()), 69 | plugin.description, 70 | plugin.id, 71 | plugin.can_uninstall.to_string(), 72 | plugin.has_image.to_string(), 73 | plugin.status, 74 | ]); 75 | } 76 | println!("{table}"); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/entities/repository_details.rs: -------------------------------------------------------------------------------- 1 | use comfy_table::{ContentArrangement, Table}; 2 | 3 | pub type RepositoryDetailsRoot = Vec; 4 | 5 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 6 | #[serde(rename_all = "camelCase")] 7 | pub struct RepositoryDetails { 8 | #[serde(rename = "Name")] 9 | pub name: String, 10 | #[serde(rename = "Url")] 11 | pub url: String, 12 | #[serde(rename = "Enabled")] 13 | pub enabled: bool, 14 | } 15 | 16 | impl RepositoryDetails { 17 | pub fn new(name: String, url: String, enabled: bool) -> RepositoryDetails { 18 | RepositoryDetails { name, url, enabled } 19 | } 20 | 21 | pub fn csv_print(repos: Vec) { 22 | for repo in repos { 23 | println!("{}, {}, {}", 24 | repo.name, 25 | repo.url, 26 | repo.enabled, 27 | ) 28 | } 29 | } 30 | 31 | pub fn json_print(repos: &[RepositoryDetails]) { 32 | println!("{}", serde_json::to_string_pretty(&repos).unwrap()); 33 | } 34 | 35 | pub fn table_print(repos: Vec) { 36 | let mut table = Table::new(); 37 | table 38 | .set_content_arrangement(ContentArrangement::Dynamic) 39 | .set_width(120) 40 | .set_header(vec![ 41 | "Plugin Name", 42 | "Version", 43 | "Config Filename", 44 | ]); 45 | for repo in repos { 46 | table.add_row(vec![repo.name, repo.url, repo.enabled.to_string()]); 47 | } 48 | println!("{table}"); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/entities/server_details.rs: -------------------------------------------------------------------------------- 1 | use serde_derive::Deserialize; 2 | use serde_derive::Serialize; 3 | 4 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 5 | #[serde(rename_all = "camelCase")] 6 | pub struct Root { 7 | #[serde(rename = "LocalAddress")] 8 | pub local_address: String, 9 | #[serde(rename = "ServerName")] 10 | pub server_name: String, 11 | #[serde(rename = "Version")] 12 | pub version: String, 13 | #[serde(rename = "ProductName")] 14 | pub product_name: String, 15 | #[serde(rename = "OperatingSystem")] 16 | pub operating_system: String, 17 | #[serde(rename = "Id")] 18 | pub id: String, 19 | #[serde(rename = "StartupWizardCompleted")] 20 | pub startup_wizard_completed: bool, 21 | #[serde(rename = "OperatingSystemDisplayName")] 22 | pub operating_system_display_name: String, 23 | #[serde(rename = "PackageName")] 24 | pub package_name: String, 25 | #[serde(rename = "HasPendingRestart")] 26 | pub has_pending_restart: bool, 27 | #[serde(rename = "IsShuttingDown")] 28 | pub is_shutting_down: bool, 29 | #[serde(rename = "SupportsLibraryMonitor")] 30 | pub supports_library_monitor: bool, 31 | #[serde(rename = "WebSocketPortNumber")] 32 | pub web_socket_port_number: i64, 33 | #[serde(rename = "CompletedInstallations")] 34 | pub completed_installations: Vec, 35 | #[serde(rename = "CanSelfRestart")] 36 | pub can_self_restart: bool, 37 | #[serde(rename = "CanLaunchWebBrowser")] 38 | pub can_launch_web_browser: bool, 39 | #[serde(rename = "ProgramDataPath")] 40 | pub program_data_path: String, 41 | #[serde(rename = "WebPath")] 42 | pub web_path: String, 43 | #[serde(rename = "ItemsByNamePath")] 44 | pub items_by_name_path: String, 45 | #[serde(rename = "CachePath")] 46 | pub cache_path: String, 47 | #[serde(rename = "LogPath")] 48 | pub log_path: String, 49 | #[serde(rename = "InternalMetadataPath")] 50 | pub internal_metadata_path: String, 51 | #[serde(rename = "TranscodingTempPath")] 52 | pub transcoding_temp_path: String, 53 | #[serde(rename = "CastReceiverApplications")] 54 | pub cast_receiver_applications: Vec, 55 | #[serde(rename = "HasUpdateAvailable")] 56 | pub has_update_available: bool, 57 | #[serde(rename = "EncoderLocation")] 58 | pub encoder_location: String, 59 | #[serde(rename = "SystemArchitecture")] 60 | pub system_architecture: String, 61 | } 62 | 63 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 64 | #[serde(rename_all = "camelCase")] 65 | pub struct CompletedInstallation { 66 | #[serde(rename = "Guid")] 67 | pub guid: String, 68 | #[serde(rename = "Name")] 69 | pub name: String, 70 | #[serde(rename = "Version")] 71 | pub version: String, 72 | #[serde(rename = "Changelog")] 73 | pub changelog: String, 74 | #[serde(rename = "SourceUrl")] 75 | pub source_url: String, 76 | #[serde(rename = "Checksum")] 77 | pub checksum: String, 78 | #[serde(rename = "PackageInfo")] 79 | pub package_info: PackageInfo, 80 | } 81 | 82 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 83 | #[serde(rename_all = "camelCase")] 84 | pub struct PackageInfo { 85 | pub name: String, 86 | pub description: String, 87 | pub overview: String, 88 | pub owner: String, 89 | pub category: String, 90 | pub guid: String, 91 | pub versions: Vec, 92 | pub image_url: String, 93 | } 94 | 95 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 96 | #[serde(rename_all = "camelCase")] 97 | pub struct Version { 98 | pub version: String, 99 | #[serde(rename = "VersionNumber")] 100 | pub version_number: String, 101 | pub changelog: String, 102 | pub target_abi: String, 103 | pub source_url: String, 104 | pub checksum: String, 105 | pub timestamp: String, 106 | pub repository_name: String, 107 | pub repository_url: String, 108 | } 109 | 110 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 111 | #[serde(rename_all = "camelCase")] 112 | pub struct CastReceiverApplication { 113 | #[serde(rename = "Id")] 114 | pub id: String, 115 | #[serde(rename = "Name")] 116 | pub name: String, 117 | } 118 | -------------------------------------------------------------------------------- /src/entities/server_info.rs: -------------------------------------------------------------------------------- 1 | pub struct ServerInfo { 2 | pub server_url: String, 3 | pub api_key: String, 4 | } 5 | 6 | impl ServerInfo { 7 | pub fn new(endpoint: &str, server_url: &str, api_key: &str) -> ServerInfo { 8 | ServerInfo { 9 | server_url: format!("{}{}", server_url, endpoint), 10 | api_key: api_key.to_owned(), 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/entities/task_details.rs: -------------------------------------------------------------------------------- 1 | use comfy_table::{ContentArrangement, Table}; 2 | 3 | #[derive(Serialize, Deserialize)] 4 | pub struct TaskDetails { 5 | #[serde(rename = "Name")] 6 | pub name: String, 7 | #[serde(rename = "State")] 8 | pub state: String, 9 | #[serde(rename = "CurrentProgressPercentage", default)] 10 | //pub percent_complete: Option, 11 | pub percent_complete: f32, 12 | #[serde(rename = "Id")] 13 | pub id: String, 14 | } 15 | 16 | impl TaskDetails { 17 | pub fn new(name: String, state: String, percent_complete: f32, id: String) -> TaskDetails { 18 | TaskDetails { 19 | name, 20 | state, 21 | percent_complete, 22 | id, 23 | } 24 | } 25 | 26 | pub fn json_print(tasks: &[TaskDetails]) { 27 | println!("{}", serde_json::to_string_pretty(&tasks).unwrap()); 28 | } 29 | 30 | pub fn csv_print(tasks: &[TaskDetails]) { 31 | for task in tasks { 32 | let mut per_comp: String = "".to_string(); 33 | if task.percent_complete > 0.0 { 34 | per_comp = task.percent_complete.to_string(); 35 | } 36 | println!("{}, {}, {}, {}", task.name, task.state, per_comp, task.id); 37 | } 38 | } 39 | pub fn table_print(tasks: Vec) { 40 | let mut table = Table::new(); 41 | table 42 | .set_content_arrangement(ContentArrangement::Dynamic) 43 | .set_width(120) 44 | .set_header(vec!["Task Name", "State", "% Complete", "Id"]); 45 | for task in tasks { 46 | let mut per_comp: String = "".to_string(); 47 | if task.percent_complete > 0.0 { 48 | per_comp = task.percent_complete.to_string(); 49 | } 50 | // table.add_row(vec![task.name, task.state, task.percent_complete.unwrap_or_else(|| "".to_owned()), task.id]); 51 | table.add_row(vec![task.name, task.state, per_comp, task.id]); 52 | } 53 | println!("{table}"); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/entities/token_details.rs: -------------------------------------------------------------------------------- 1 | use serde_derive::Deserialize; 2 | use serde_derive::Serialize; 3 | 4 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 5 | #[serde(rename_all = "camelCase")] 6 | pub struct TokenDetails { 7 | #[serde(rename = "Items")] 8 | pub items: Vec, 9 | #[serde(rename = "TotalRecordCount")] 10 | pub total_record_count: i64, 11 | #[serde(rename = "StartIndex")] 12 | pub start_index: i64, 13 | } 14 | 15 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 16 | #[serde(rename_all = "camelCase")] 17 | pub struct TokenItem { 18 | #[serde(rename = "Id")] 19 | pub id: i64, 20 | #[serde(rename = "AccessToken")] 21 | pub access_token: String, 22 | #[serde(rename = "DeviceId")] 23 | pub device_id: String, 24 | #[serde(rename = "AppName")] 25 | pub app_name: String, 26 | #[serde(rename = "AppVersion")] 27 | pub app_version: String, 28 | #[serde(rename = "DeviceName")] 29 | pub device_name: String, 30 | #[serde(rename = "UserId")] 31 | pub user_id: String, 32 | #[serde(rename = "IsActive")] 33 | pub is_active: bool, 34 | #[serde(rename = "DateCreated")] 35 | pub date_created: String, 36 | #[serde(rename = "DateRevoked", default)] 37 | pub date_revoked: String, 38 | #[serde(rename = "DateLastActivity", default)] 39 | pub date_last_activity: String, 40 | #[serde(rename = "UserName", default)] 41 | pub user_name: String, 42 | } 43 | -------------------------------------------------------------------------------- /src/entities/user_details.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, Serialize, Deserialize)] 2 | pub struct UserDetails { 3 | #[serde(rename = "Name")] 4 | pub name: String, 5 | #[serde(rename = "ServerId")] 6 | pub server_id: String, 7 | #[serde(rename = "ServerName", default)] 8 | pub server_name: String, 9 | #[serde(rename = "Id")] 10 | pub id: String, 11 | #[serde(rename = "PrimaryImageTag", default)] 12 | pub primary_image_tag: String, 13 | #[serde(rename = "HasPassword")] 14 | pub has_password: bool, 15 | #[serde(rename = "HasConfiguredPassword")] 16 | pub has_configured_password: bool, 17 | #[serde(rename = "HasConfiguredEasyPassword")] 18 | pub has_configured_easy_password: bool, 19 | #[serde(rename = "EnableAutoLogin")] 20 | pub enable_auto_login: bool, 21 | #[serde(rename = "LastLoginDate", default)] 22 | pub last_login_date: Option, 23 | #[serde(rename = "LastActivityDate", default)] 24 | pub last_activity_date: Option, 25 | #[serde(rename = "Configuration")] 26 | pub configuration: Configuration, 27 | #[serde(rename = "Policy")] 28 | pub policy: Policy, 29 | #[serde(rename = "PrimaryImageAspectRatio", default)] 30 | pub primary_image_aspect_ratio: i64, 31 | } 32 | 33 | // Struct to contain the Policy information that is a part of the user details. 34 | #[derive(Debug, Serialize, Deserialize)] 35 | pub struct Policy { 36 | #[serde(rename = "IsAdministrator")] 37 | pub is_administrator: bool, 38 | #[serde(rename = "IsHidden")] 39 | pub is_hidden: bool, 40 | #[serde(rename = "IsDisabled")] 41 | pub is_disabled: bool, 42 | #[serde(rename = "MaxParentalRating", default)] 43 | pub max_parental_rating: i64, 44 | #[serde(rename = "BlockedTags")] 45 | pub blocked_tags: Vec, 46 | #[serde(rename = "EnableUserPreferenceAccess")] 47 | pub enable_user_preference_access: bool, 48 | #[serde(rename = "AccessSchedules")] 49 | pub access_schedules: Vec, 50 | #[serde(rename = "BlockUnratedItems")] 51 | pub block_unrated_items: Vec, 52 | #[serde(rename = "EnableRemoteControlOfOtherUsers")] 53 | pub enable_remote_control_of_other_users: bool, 54 | #[serde(rename = "EnableSharedDeviceControl")] 55 | pub enable_shared_device_control: bool, 56 | #[serde(rename = "EnableRemoteAccess")] 57 | pub enable_remote_access: bool, 58 | #[serde(rename = "EnableLiveTvManagement")] 59 | pub enable_live_tv_management: bool, 60 | #[serde(rename = "EnableLiveTvAccess")] 61 | pub enable_live_tv_access: bool, 62 | #[serde(rename = "EnableMediaPlayback")] 63 | pub enable_media_playback: bool, 64 | #[serde(rename = "EnableAudioPlaybackTranscoding")] 65 | pub enable_audio_playback_transcoding: bool, 66 | #[serde(rename = "EnableVideoPlaybackTranscoding")] 67 | pub enable_video_playback_transcoding: bool, 68 | #[serde(rename = "EnablePlaybackRemuxing")] 69 | pub enable_playback_remuxing: bool, 70 | #[serde(rename = "ForceRemoteSourceTranscoding")] 71 | pub force_remote_source_transcoding: bool, 72 | #[serde(rename = "EnableContentDeletion")] 73 | pub enable_content_deletion: bool, 74 | #[serde(rename = "EnableContentDeletionFromFolders")] 75 | pub enable_content_deletion_from_folders: Vec, 76 | #[serde(rename = "EnableContentDownloading")] 77 | pub enable_content_downloading: bool, 78 | #[serde(rename = "EnableSyncTranscoding")] 79 | pub enable_sync_transcoding: bool, 80 | #[serde(rename = "EnableMediaConversion")] 81 | pub enable_media_conversion: bool, 82 | #[serde(rename = "EnabledDevices")] 83 | pub enabled_devices: Vec, 84 | #[serde(rename = "EnableAllDevices")] 85 | pub enable_all_devices: bool, 86 | #[serde(rename = "EnabledChannels")] 87 | pub enabled_channels: Vec, 88 | #[serde(rename = "EnableAllChannels")] 89 | pub enable_all_channels: bool, 90 | #[serde(rename = "EnabledFolders")] 91 | pub enabled_folders: Vec, 92 | #[serde(rename = "EnableAllFolders")] 93 | pub enable_all_folders: bool, 94 | #[serde(rename = "InvalidLoginAttemptCount")] 95 | pub invalid_login_attempt_count: i64, 96 | #[serde(rename = "LoginAttemptsBeforeLockout")] 97 | pub login_attempts_before_lockout: i64, 98 | #[serde(rename = "MaxActiveSessions")] 99 | pub max_active_sessions: i64, 100 | #[serde(rename = "EnablePublicSharing")] 101 | pub enable_public_sharing: bool, 102 | #[serde(rename = "BlockedMediaFolders")] 103 | pub blocked_media_folders: Vec, 104 | #[serde(rename = "BlockedChannels")] 105 | pub blocked_channels: Vec, 106 | #[serde(rename = "RemoteClientBitrateLimit")] 107 | pub remote_client_bitrate_limit: i64, 108 | #[serde(rename = "AuthenticationProviderId")] 109 | pub authentication_provider_id: String, 110 | #[serde(rename = "PasswordResetProviderId")] 111 | pub password_reset_provider_id: String, 112 | #[serde(rename = "SyncPlayAccess")] 113 | pub sync_play_access: String, 114 | } 115 | 116 | #[derive(Debug, Serialize, Deserialize)] 117 | pub struct Configuration { 118 | #[serde(rename = "AudioLanguagePreference", default)] 119 | pub audio_language_preference: String, 120 | #[serde(rename = "PlayDefaultAudioTrack")] 121 | pub play_default_audio_track: bool, 122 | #[serde(rename = "SubtitleLanguagePreference")] 123 | pub subtitle_language_preference: String, 124 | #[serde(rename = "DisplayMissingEpisodes")] 125 | pub display_missing_episodes: bool, 126 | #[serde(rename = "GroupedFolders")] 127 | pub grouped_folders: Vec, 128 | #[serde(rename = "SubtitleMode")] 129 | pub subtitle_mode: String, 130 | #[serde(rename = "DisplayCollectionsView")] 131 | pub display_collections_view: bool, 132 | #[serde(rename = "EnableLocalPassword")] 133 | pub enable_local_password: bool, 134 | #[serde(rename = "OrderedViews")] 135 | pub ordered_views: Vec, 136 | #[serde(rename = "LatestItemsExcludes")] 137 | pub latest_items_excludes: Vec, 138 | #[serde(rename = "MyMediaExcludes")] 139 | pub my_media_excludes: Vec, 140 | #[serde(rename = "HidePlayedInLatest")] 141 | pub hide_played_in_latest: bool, 142 | #[serde(rename = "RememberAudioSelections")] 143 | pub remember_audio_selections: bool, 144 | #[serde(rename = "RememberSubtitleSelections")] 145 | pub remember_subtitle_selections: bool, 146 | #[serde(rename = "EnableNextEpisodeAutoPlay")] 147 | pub enable_next_episode_auto_play: bool, 148 | } 149 | 150 | #[derive(Debug, Serialize, Deserialize)] 151 | pub struct AccessSchedule { 152 | #[serde(rename = "UserId")] 153 | pub user_id: String, 154 | #[serde(rename = "DayOfWeek")] 155 | pub day_of_week: String, 156 | #[serde(rename = "StartHour")] 157 | pub start_hour: i64, 158 | #[serde(rename = "EndHour")] 159 | pub end_hour: i64, 160 | } 161 | impl UserDetails { 162 | pub fn json_print_user(user: &UserDetails) { 163 | println!("{}", serde_json::to_string_pretty(&user).unwrap()); 164 | } 165 | 166 | pub fn json_print_users(users: &[UserDetails]) { 167 | println!("{}", serde_json::to_string_pretty(&users).unwrap()); 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use base64::{engine::general_purpose, Engine as _}; 2 | use clap::{CommandFactory, Parser, Subcommand, ValueEnum}; 3 | use clap_complete::{generate, Shell}; 4 | use image::ImageFormat; 5 | use std::env; 6 | use std::fmt; 7 | use std::fs::{self, File}; 8 | use std::io::{self, BufRead, BufReader, Cursor, Read, Write}; 9 | use std::{thread, time}; 10 | 11 | mod user_actions; 12 | use user_actions::{UserAuth, UserList, UserWithPass}; 13 | mod system_actions; 14 | use system_actions::*; 15 | mod plugin_actions; 16 | use plugin_actions::PluginInfo; 17 | mod entities; 18 | mod responder; 19 | use entities::activity_details::ActivityDetails; 20 | use entities::device_details::{DeviceDetails, DeviceRootJson}; 21 | use entities::library_details::{LibraryDetails, LibraryRootJson}; 22 | use entities::log_details::LogDetails; 23 | use entities::media_details::MediaRoot; 24 | use entities::movie_details::MovieDetails; 25 | use entities::package_details::{PackageDetails, PackageDetailsRoot}; 26 | use entities::plugin_details::{PluginDetails, PluginRootJson}; 27 | use entities::repository_details::{RepositoryDetails, RepositoryDetailsRoot}; 28 | use entities::server_info::ServerInfo; 29 | use entities::task_details::TaskDetails; 30 | use entities::user_details::{Policy, UserDetails}; 31 | mod utils; 32 | use utils::output_writer::export_data; 33 | use utils::status_handler::{handle_others, handle_unauthorized}; 34 | 35 | #[macro_use] 36 | extern crate serde_derive; 37 | 38 | // 39 | // Global variables for API endpoints 40 | // 41 | const USER_POLICY: &str = "/Users/{userId}/Policy"; 42 | const USER_ID: &str = "/Users/{userId}"; 43 | const USERS: &str = "/Users"; 44 | const DEVICES: &str = "/Devices"; 45 | 46 | #[derive(Debug, Clone, Serialize, Deserialize)] 47 | #[serde(default)] 48 | pub struct AppConfig { 49 | status: String, 50 | comfy: bool, 51 | server_url: String, 52 | os: String, 53 | api_key: String, 54 | token: String, 55 | } 56 | 57 | impl Default for AppConfig { 58 | fn default() -> Self { 59 | AppConfig { 60 | status: "not configured".to_owned(), 61 | comfy: true, 62 | server_url: "Unknown".to_owned(), 63 | os: "Unknown".to_owned(), 64 | api_key: "Unknown".to_owned(), 65 | token: "Unknown".to_owned(), 66 | } 67 | } 68 | } 69 | 70 | /// CLAP CONFIGURATION 71 | /// CLI controller for Jellyfin 72 | #[derive(Debug, Parser)] // requires `derive` feature 73 | #[clap(name = "jellyroller", author, version)] 74 | #[clap(about = "A CLI controller for managing Jellyfin", long_about = None)] 75 | struct Cli { 76 | #[clap(subcommand)] 77 | command: Commands, 78 | } 79 | 80 | #[derive(Debug, Subcommand)] 81 | enum Commands { 82 | /// Creates a new user 83 | #[clap(arg_required_else_help = true)] 84 | AddUser { 85 | /// Username to create. 86 | #[clap(required = true, value_parser)] 87 | username: String, 88 | /// Password for created user. 89 | #[clap(required = true, value_parser)] 90 | password: String, 91 | }, 92 | /// Uses the supplied file to mass create new users. 93 | AddUsers { 94 | /// File that contains the user information in "username,password" lines. 95 | #[clap(required = true, value_parser)] 96 | inputfile: String, 97 | }, 98 | /// Generate Shell completions 99 | Completions { 100 | #[clap(required = true, value_parser)] 101 | shell: Shell, 102 | }, 103 | /// Creates a report of either activity or available movie items 104 | CreateReport { 105 | /// Type of report (activity or movie) 106 | #[clap(required = true)] 107 | report_type: ReportType, 108 | /// Total number of records to return (defaults to 100) 109 | #[clap(required = false, short, long, default_value = "100")] 110 | limit: String, 111 | /// Output filename 112 | #[clap(required = false, short, long, default_value = "")] 113 | filename: String, 114 | }, 115 | /// Deletes an existing user. 116 | #[clap(arg_required_else_help = true)] 117 | DeleteUser { 118 | /// User to remove. 119 | #[clap(required = true, value_parser)] 120 | username: String, 121 | }, 122 | /// Disable a user. 123 | DisableUser { 124 | #[clap(required = true, value_parser)] 125 | username: String, 126 | }, 127 | /// Enable a user. 128 | EnableUser { 129 | #[clap(required = true, value_parser)] 130 | username: String, 131 | }, 132 | /// Executes a scheduled task by name. 133 | ExecuteTaskByName { 134 | #[clap(required = true, value_parser)] 135 | task: String, 136 | }, 137 | /// Generate a report for an issue. 138 | GenerateReport {}, 139 | /// Show all devices. 140 | GetDevices { 141 | /// Only show devices active in the last hour 142 | #[clap(long, required = false)] 143 | active: bool, 144 | /// Print information as json. 145 | #[clap(long, required = false)] 146 | json: bool, 147 | /// Specify the output format 148 | #[clap(short = 'o', long, value_enum, default_value = "table")] 149 | output_format: OutputFormat, 150 | }, 151 | /// Gets the libraries available to the configured user 152 | GetLibraries { 153 | /// Print information as json. 154 | #[clap(long, required = false)] 155 | json: bool, 156 | /// Specify the output format 157 | #[clap(short = 'o', long, value_enum, default_value = "table")] 158 | output_format: OutputFormat, 159 | }, 160 | /// Lists all available packages 161 | GetPackages { 162 | /// Print information as json. 163 | #[clap(long, required = false)] 164 | json: bool, 165 | /// Specify the output format 166 | #[clap(short = 'o', long, value_enum, default_value = "table")] 167 | output_format: OutputFormat, 168 | }, 169 | /// Returns a list of installed plugins 170 | GetPlugins { 171 | /// Print information as json. 172 | #[clap(long, required = false)] 173 | json: bool, 174 | /// Specify the output format 175 | #[clap(short = 'o', long, value_enum, default_value = "table")] 176 | output_format: OutputFormat, 177 | }, 178 | /// Lists all current repositories 179 | GetRepositories { 180 | /// Print information as json. 181 | #[clap(long, required = false)] 182 | json: bool, 183 | /// Specify the output format 184 | #[clap(short = 'o', long, value_enum, default_value = "table")] 185 | output_format: OutputFormat, 186 | }, 187 | /// Show all scheduled tasks and their status. 188 | GetScheduledTasks { 189 | /// Print information as json (DEPRECATED). 190 | #[clap(long, required = false)] 191 | json: bool, 192 | /// Specify the output format 193 | #[clap(short = 'o', long, value_enum, default_value = "table")] 194 | output_format: OutputFormat, 195 | }, 196 | /// Grants the specified user admin rights. 197 | GrantAdmin { 198 | #[clap(required = true, value_parser)] 199 | username: String, 200 | }, 201 | /// Perform a silent initialization. 202 | Initialize { 203 | /// Username for API key creation 204 | #[clap(required = true, long = "username")] 205 | username: String, 206 | /// Password for user 207 | #[clap(required = true, long = "password")] 208 | password: String, 209 | /// URL of server 210 | #[clap(required = true, long = "url")] 211 | server_url: String 212 | }, 213 | /// Installs the specified package 214 | InstallPackage { 215 | /// Package to install 216 | #[clap(short = 'p', long = "package", required = true)] 217 | package: String, 218 | /// Version to install 219 | #[clap(short = 'v', long = "version", required = false, default_value = "")] 220 | version: String, 221 | /// Repository to install from 222 | #[clap(short = 'r', long = "repository", required = false, default_value = "")] 223 | repository: String, 224 | }, 225 | /// Displays the available system logs. 226 | ListLogs { 227 | /// Print information as json. 228 | #[clap(long, required = false)] 229 | json: bool, 230 | /// Specify the output format 231 | #[clap(short = 'o', long, value_enum, default_value = "table")] 232 | output_format: OutputFormat, 233 | }, 234 | /// Lists the current users with basic information. 235 | ListUsers { 236 | /// Exports the user list information to a file 237 | #[clap(short, long)] 238 | export: bool, 239 | /// Path for the file export 240 | #[clap(short, long, default_value = "")] 241 | output: String, 242 | /// Username to gather information about 243 | #[clap(short, long, default_value = "")] 244 | username: String, 245 | }, 246 | /// Reconfigure the connection information. 247 | Reconfigure {}, 248 | /// Registers a new library. 249 | RegisterLibrary { 250 | /// Name of the new library 251 | #[clap(required = true, short = 'n', long)] 252 | name: String, 253 | /// Collection Type of the new library 254 | #[clap(required = true, short = 'c', long)] 255 | collectiontype: CollectionType, 256 | /// Path to file that contains the JSON for the library 257 | #[clap(required = true, short = 'f', long)] 258 | filename: String, 259 | }, 260 | /// Registers a new Plugin Repository 261 | RegisterRepository { 262 | /// Name of the new repository 263 | #[clap(required = true, short = 'n', long = "name")] 264 | name: String, 265 | /// URL of the new repository 266 | #[clap(required = true, short = 'u', long = "url")] 267 | path: String, 268 | }, 269 | /// Removes all devices associated with the specified user. 270 | RemoveDeviceByUsername { 271 | #[clap(required = true, value_parser)] 272 | username: String, 273 | }, 274 | /// Resets a user's password. 275 | #[clap(arg_required_else_help = true)] 276 | ResetPassword { 277 | /// User to be modified. 278 | #[clap(required = true, value_parser)] 279 | username: String, 280 | /// What to reset the specified user's password to. 281 | #[clap(required = true, value_parser)] 282 | password: String, 283 | }, 284 | /// Revokes admin rights from the specified user. 285 | RevokeAdmin { 286 | #[clap(required = true, value_parser)] 287 | username: String, 288 | }, 289 | /// Restarts Jellyfin 290 | RestartJellyfin {}, 291 | /// Start a library scan. 292 | ScanLibrary { 293 | /// Library ID 294 | #[clap(required = false, value_parser, default_value = "all")] 295 | library_id: String, 296 | /// Type of scan 297 | #[clap(required = false, default_value = "all")] 298 | scan_type: ScanType, 299 | }, 300 | /// Executes a search of your media 301 | SearchMedia { 302 | /// Search term 303 | #[clap(required = true, short, long)] 304 | term: String, 305 | /// Filter for media type 306 | #[clap(required = false, short, long, default_value = "all")] 307 | mediatype: String, 308 | #[clap(required = false, short, long, default_value = "")] 309 | parentid: String, 310 | #[clap(short = 'o', long, value_enum, default_value = "table")] 311 | output_format: OutputFormat, 312 | /// By default, the server does not include file paths in the search results. Setting this 313 | /// will tell the server to include the file path in the search results. 314 | #[clap(short = 'f', long, required = false)] 315 | include_filepath: bool, 316 | /// Available columns: Name, Id, Type, Path, CriticRating, ProductionYear 317 | #[clap(short = 'c', long, value_parser, num_args = 0.., value_delimiter = ',', default_value = "Name,ID,Type")] 318 | table_columns: Vec, 319 | }, 320 | /// Displays the server information. 321 | ServerInfo {}, 322 | /// Displays the requested logfile. 323 | ShowLog { 324 | /// Name of the logfile to show. 325 | #[clap(required = true, value_parser)] 326 | logfile: String, 327 | }, 328 | /// Shuts down Jellyfin 329 | ShutdownJellyfin {}, 330 | /// Updates image of specified file by id 331 | UpdateImageById { 332 | /// Attempt to update based on item id. 333 | #[clap(required = true, short = 'i', long)] 334 | id: String, 335 | /// Path to the image that will be used. 336 | #[clap(required = true, short, long)] 337 | path: String, 338 | #[clap(required = true, short = 'I', long)] 339 | imagetype: ImageType, 340 | }, 341 | /// Updates image of specified file by name 342 | UpdateImageByName { 343 | /// Attempt to update based on title. Requires unique search term. 344 | #[clap(required = true, short, long)] 345 | title: String, 346 | /// Path to the image that will be used. 347 | #[clap(required = true, short, long)] 348 | path: String, 349 | #[clap(required = true, short, long)] 350 | imagetype: ImageType, 351 | }, 352 | /// Updates metadata of specified id with metadata provided by specified file 353 | UpdateMetadata { 354 | /// ID of the file to update 355 | #[clap(required = true, short = 'i', long)] 356 | id: String, 357 | /// File that contains the metadata to upload to the server 358 | #[clap(required = true, short = 'f', long)] 359 | filename: String, 360 | }, 361 | /// Mass update users in the supplied file 362 | UpdateUsers { 363 | /// File that contains the user JSON information. 364 | #[clap(required = true, value_parser)] 365 | inputfile: String, 366 | } 367 | } 368 | 369 | #[derive(ValueEnum, Clone, Debug, PartialEq)] 370 | enum CollectionType { 371 | Movies, 372 | TVShows, 373 | Music, 374 | MusicVideos, 375 | HomeVideos, 376 | BoxSets, 377 | Books, 378 | Mixed, 379 | } 380 | 381 | #[derive(ValueEnum, Clone, Debug, PartialEq)] 382 | enum Detail { 383 | User, 384 | Server, 385 | } 386 | 387 | #[derive(ValueEnum, Clone, Debug, PartialEq)] 388 | enum ImageType { 389 | Primary, 390 | Art, 391 | Backdrop, 392 | Banner, 393 | Logo, 394 | Thumb, 395 | Disc, 396 | Box, 397 | Screenshot, 398 | Menu, 399 | BoxRear, 400 | Profile, 401 | } 402 | 403 | #[derive(ValueEnum, Clone, Debug)] 404 | enum OutputFormat { 405 | Json, 406 | Csv, 407 | Table, 408 | } 409 | 410 | #[derive(ValueEnum, Clone, Debug, PartialEq)] 411 | enum ReportType { 412 | Activity, 413 | Movie, 414 | } 415 | 416 | #[derive(ValueEnum, Clone, Debug, PartialEq)] 417 | enum ScanType { 418 | NewUpdated, 419 | MissingMetadata, 420 | ReplaceMetadata, 421 | All, 422 | } 423 | 424 | fn main() -> Result<(), confy::ConfyError> { 425 | let mut current = env::current_exe().unwrap(); 426 | current.pop(); 427 | current.push("jellyroller.config"); 428 | 429 | let mut cfg: AppConfig = if std::path::Path::new(current.as_path()).exists() { 430 | confy::load_path(current.as_path())? 431 | } else { 432 | confy::load("jellyroller", "jellyroller")? 433 | }; 434 | 435 | // Due to an oddity with confy and clap, manually check for help flag. 436 | let args: Vec = env::args().collect(); 437 | if !(args.contains(&"initialize".to_string()) || args.contains(&"-h".to_string()) || args.contains(&"--help".to_string())) { 438 | if cfg.status == "not configured" { 439 | println!("Application is not configured!"); 440 | initial_config(cfg); 441 | std::process::exit(0); 442 | } else if cfg.token == "Unknown" { 443 | println!("[INFO] Username/Password detected. Reconfiguring to use API key."); 444 | token_to_api(cfg.clone()); 445 | } 446 | } 447 | 448 | let args = Cli::parse(); 449 | 450 | match args.command { 451 | Commands::Initialize { 452 | username, 453 | password, 454 | server_url 455 | } => { 456 | println!("Configuring JellyRoller with supplied values....."); 457 | env::consts::OS.clone_into(&mut cfg.os); 458 | server_url.trim().clone_into(&mut cfg.server_url); 459 | cfg.api_key = UserAuth::auth_user(UserAuth::new(&cfg.server_url, username.trim(), password)) 460 | .expect("Unable to generate user auth token. Please assure your configuration information was input correctly\n"); 461 | "configured".clone_into(&mut cfg.status); 462 | token_to_api(cfg); 463 | } 464 | 465 | //TODO: Create a simple_post variation that allows for query params. 466 | Commands::RegisterLibrary { 467 | name, 468 | collectiontype, 469 | filename, 470 | } => { 471 | let mut endpoint = String::from("/Library/VirtualFolders?CollectionType="); 472 | endpoint.push_str(collectiontype.to_string().as_str()); 473 | endpoint.push_str("&refreshLibrary=true"); 474 | endpoint.push_str("&name="); 475 | endpoint.push_str(name.as_str()); 476 | let mut file = File::open(filename).expect("Unable to open file."); 477 | let mut contents = String::new(); 478 | file.read_to_string(&mut contents) 479 | .expect("Unable to read file."); 480 | register_library( 481 | ServerInfo::new(endpoint.as_str(), &cfg.server_url, &cfg.api_key), 482 | contents, 483 | ) 484 | } 485 | 486 | Commands::GenerateReport {} => { 487 | let info = return_server_info(ServerInfo::new( 488 | "/System/Info", 489 | &cfg.server_url, 490 | &cfg.api_key, 491 | )); 492 | let json: serde_json::Value = serde_json::from_str(info.as_str()).expect("failed"); 493 | println!( 494 | "\ 495 | Please copy/paste the following information to any issue that is being opened:\n\ 496 | JellyRoller Version: {}\n\ 497 | JellyRoller OS: {}\n\ 498 | Jellyfin Version: {}\n\ 499 | Jellyfin Host OK: {}\n\ 500 | Jellyfin Server Architecture: {}\ 501 | ", 502 | env!("CARGO_PKG_VERSION"), 503 | env::consts::OS, 504 | json.get("Version") 505 | .expect("Unable to extract Jellyfin version."), 506 | json.get("OperatingSystem") 507 | .expect("Unable to extract Jellyfin OS information."), 508 | json.get("SystemArchitecture") 509 | .expect("Unable to extract Jellyfin System Architecture.") 510 | ); 511 | } 512 | 513 | Commands::UpdateMetadata { id, filename } => { 514 | // Read the JSON file and prepare it for upload. 515 | let json: String = fs::read_to_string(filename).unwrap(); 516 | update_metadata( 517 | ServerInfo::new("/Items/{itemId}", &cfg.server_url, &cfg.api_key), 518 | id, 519 | json, 520 | ); 521 | } 522 | Commands::UpdateImageByName { 523 | title, 524 | path, 525 | imagetype, 526 | } => { 527 | let search: MediaRoot = 528 | execute_search(&title, "all".to_string(), "".to_string(), false, &cfg); 529 | if search.total_record_count > 1 { 530 | eprintln!( 531 | "Too many results found. Updating by name requires a unique search term." 532 | ); 533 | std::process::exit(1); 534 | } 535 | let img_base64 = image_to_base64(path); 536 | for item in search.items { 537 | update_image( 538 | ServerInfo::new( 539 | "/Items/{itemId}/Images/{imageType}", 540 | &cfg.server_url, 541 | &cfg.api_key, 542 | ), 543 | item.id, 544 | &imagetype, 545 | &img_base64, 546 | ); 547 | } 548 | } 549 | Commands::UpdateImageById { 550 | id, 551 | path, 552 | imagetype, 553 | } => { 554 | let img_base64 = image_to_base64(path); 555 | update_image( 556 | ServerInfo::new( 557 | "/Items/{itemId}/Images/{imageType}", 558 | &cfg.server_url, 559 | &cfg.api_key, 560 | ), 561 | id, 562 | &imagetype, 563 | &img_base64, 564 | ); 565 | } 566 | 567 | // User based commands 568 | Commands::AddUser { username, password } => { 569 | add_user(&cfg, username, password); 570 | } 571 | Commands::DeleteUser { username } => { 572 | let user_id = get_user_id(&cfg, &username); 573 | let server_path = format!("{}/Users/{}", cfg.server_url, user_id); 574 | match UserWithPass::delete_user(UserWithPass::new( 575 | Some(username), 576 | None, 577 | None, 578 | server_path, 579 | cfg.api_key, 580 | )) { 581 | Err(_) => { 582 | eprintln!("Unable to delete user."); 583 | std::process::exit(1); 584 | } 585 | Ok(i) => i, 586 | } 587 | } 588 | Commands::ListUsers { 589 | export, 590 | mut output, 591 | username, 592 | } => { 593 | if username.is_empty() { 594 | let users: Vec = 595 | match UserList::list_users(UserList::new(USERS, &cfg.server_url, &cfg.api_key)) 596 | { 597 | Err(_) => { 598 | eprintln!("Unable to gather users."); 599 | std::process::exit(1); 600 | } 601 | Ok(i) => i, 602 | }; 603 | if export { 604 | println!("Exporting all user information....."); 605 | if output.is_empty() { 606 | "exported-user-info.json".clone_into(&mut output); 607 | } 608 | let data: String = match serde_json::to_string_pretty(&users) { 609 | Err(_) => { 610 | eprintln!("Unable to convert user information into JSON."); 611 | std::process::exit(1); 612 | } 613 | Ok(i) => i, 614 | }; 615 | export_data(&data, output); 616 | } else { 617 | UserDetails::json_print_users(&users); 618 | } 619 | } else { 620 | let user_id = UserList::get_user_id( 621 | UserList::new(USERS, &cfg.server_url, &cfg.api_key), 622 | &username, 623 | ); 624 | let user = gather_user_information(&cfg, &username, &user_id); 625 | if export { 626 | println!("Exporting user information....."); 627 | if output.is_empty() { 628 | output = format!("exported-user-info-{}.json", username); 629 | } 630 | let data: String = match serde_json::to_string_pretty(&user) { 631 | Err(_) => { 632 | eprintln!("Unable to convert user information into JSON."); 633 | std::process::exit(1); 634 | } 635 | Ok(i) => i, 636 | }; 637 | export_data(&data, output); 638 | } else { 639 | UserDetails::json_print_user(&user); 640 | } 641 | } 642 | } 643 | Commands::ResetPassword { username, password } => { 644 | // Get usename 645 | let user_id = UserList::get_user_id( 646 | UserList::new(USERS, &cfg.server_url, &cfg.api_key), 647 | &username, 648 | ); 649 | // Setup the endpoint 650 | let server_path = format!("{}/Users/{}/Password", &cfg.server_url, user_id); 651 | match UserWithPass::resetpass(UserWithPass::new( 652 | None, 653 | Some(password), 654 | Some("".to_string()), 655 | server_path, 656 | cfg.api_key, 657 | )) { 658 | Err(_) => { 659 | eprintln!("Unable to convert user information into JSON."); 660 | std::process::exit(1); 661 | } 662 | Ok(i) => i, 663 | } 664 | } 665 | Commands::DisableUser { username } => { 666 | let id = get_user_id(&cfg, &username); 667 | let mut user_info = gather_user_information(&cfg, &username, &id); 668 | user_info.policy.is_disabled = true; 669 | UserList::update_user_config_bool( 670 | UserList::new(USER_POLICY, &cfg.server_url, &cfg.api_key), 671 | &user_info.policy, 672 | &id, 673 | &username, 674 | ) 675 | .expect("Unable to update user."); 676 | } 677 | Commands::EnableUser { username } => { 678 | let id = get_user_id(&cfg, &username); 679 | let mut user_info = gather_user_information(&cfg, &username, &id); 680 | user_info.policy.is_disabled = false; 681 | UserList::update_user_config_bool( 682 | UserList::new(USER_POLICY, &cfg.server_url, &cfg.api_key), 683 | &user_info.policy, 684 | &id, 685 | &username, 686 | ) 687 | .expect("Unable to update user."); 688 | } 689 | Commands::GrantAdmin { username } => { 690 | let id = get_user_id(&cfg, &username); 691 | let mut user_info = gather_user_information(&cfg, &username, &id); 692 | user_info.policy.is_administrator = true; 693 | UserList::update_user_config_bool( 694 | UserList::new(USER_POLICY, &cfg.server_url, &cfg.api_key), 695 | &user_info.policy, 696 | &id, 697 | &username, 698 | ) 699 | .expect("Unable to update user."); 700 | } 701 | Commands::RevokeAdmin { username } => { 702 | let id = get_user_id(&cfg, &username); 703 | let mut user_info = gather_user_information(&cfg, &username, &id); 704 | user_info.policy.is_administrator = false; 705 | UserList::update_user_config_bool( 706 | UserList::new(USER_POLICY, &cfg.server_url, &cfg.api_key), 707 | &user_info.policy, 708 | &id, 709 | &username, 710 | ) 711 | .expect("Unable to update user."); 712 | } 713 | Commands::AddUsers { inputfile } => { 714 | let reader = BufReader::new(File::open(inputfile).unwrap()); 715 | for line in reader.lines() { 716 | match line { 717 | Ok(l) => { 718 | let vec: Vec<&str> = l.split(',').collect(); 719 | add_user(&cfg, vec[0].to_owned(), vec[1].to_owned()); 720 | } 721 | Err(e) => println!("Unable to add user. {e}"), 722 | } 723 | } 724 | } 725 | Commands::UpdateUsers { inputfile } => { 726 | let data: String = match fs::read_to_string(inputfile) { 727 | Err(_) => { 728 | eprintln!("Unable to process input file."); 729 | std::process::exit(1); 730 | } 731 | Ok(i) => i, 732 | }; 733 | if data.starts_with('[') { 734 | let info: Vec = match serde_json::from_str::>(&data) { 735 | Err(_) => { 736 | eprintln!("Unable to convert user details JSON.."); 737 | std::process::exit(1); 738 | } 739 | Ok(i) => i, 740 | }; 741 | for item in info { 742 | match UserList::update_user_info( 743 | UserList::new(USER_ID, &cfg.server_url, &cfg.api_key), 744 | &item.id, 745 | &item, 746 | ) { 747 | Ok(_) => {} 748 | Err(e) => eprintln!("Unable to update user. {e}"), 749 | }; 750 | } 751 | } else { 752 | let info: UserDetails = match serde_json::from_str::(&data) { 753 | Err(_) => { 754 | eprintln!("Unable to convert user details JSON."); 755 | std::process::exit(1); 756 | } 757 | Ok(i) => i, 758 | }; 759 | let user_id = get_user_id(&cfg, &info.name); 760 | match UserList::update_user_info( 761 | UserList::new(USER_ID, &cfg.server_url, &cfg.api_key), 762 | &user_id, 763 | &info, 764 | ) { 765 | Ok(_) => {} 766 | Err(e) => { 767 | eprintln!("Unable to update user. {e}") 768 | } 769 | } 770 | } 771 | } 772 | 773 | // Server based commands 774 | Commands::GetPackages { 775 | json, 776 | output_format, 777 | } => { 778 | let packages = 779 | get_packages_info(ServerInfo::new("/Packages", &cfg.server_url, &cfg.api_key)) 780 | .unwrap(); 781 | 782 | if json { 783 | json_deprecation(); 784 | PackageDetails::json_print(&packages); 785 | std::process::exit(0) 786 | } 787 | 788 | match output_format { 789 | OutputFormat::Json => { 790 | PackageDetails::json_print(&packages); 791 | } 792 | OutputFormat::Csv => { 793 | PackageDetails::csv_print(packages); 794 | } 795 | _ => { 796 | PackageDetails::table_print(packages); 797 | } 798 | } 799 | } 800 | 801 | Commands::GetRepositories { 802 | json, 803 | output_format, 804 | } => { 805 | let repos = get_repo_info(ServerInfo::new( 806 | "/Repositories", 807 | &cfg.server_url, 808 | &cfg.api_key, 809 | )) 810 | .unwrap(); 811 | 812 | if json { 813 | json_deprecation(); 814 | RepositoryDetails::json_print(&repos); 815 | std::process::exit(0) 816 | } 817 | 818 | match output_format { 819 | OutputFormat::Json => { 820 | RepositoryDetails::json_print(&repos); 821 | } 822 | OutputFormat::Csv => { 823 | RepositoryDetails::csv_print(repos); 824 | } 825 | _ => { 826 | RepositoryDetails::table_print(repos); 827 | } 828 | } 829 | } 830 | 831 | Commands::RegisterRepository { name, path } => { 832 | let mut repos = get_repo_info(ServerInfo::new( 833 | "/Repositories", 834 | &cfg.server_url, 835 | &cfg.api_key, 836 | )) 837 | .unwrap(); 838 | repos.push(RepositoryDetails::new(name, path, true)); 839 | set_repo_info( 840 | ServerInfo::new("/Repositories", &cfg.server_url, &cfg.api_key), 841 | repos, 842 | ); 843 | } 844 | 845 | Commands::InstallPackage { 846 | package, 847 | version, 848 | repository, 849 | } => { 850 | // Check if package name has spaces and replace them as needed 851 | let encoded = package.replace(" ", "%20"); 852 | install_package( 853 | ServerInfo::new( 854 | "/Packages/Installed/{package}", 855 | &cfg.server_url, 856 | &cfg.api_key, 857 | ), 858 | &encoded, 859 | &version, 860 | &repository, 861 | ); 862 | } 863 | 864 | Commands::ServerInfo {} => { 865 | get_server_info(ServerInfo::new( 866 | "/System/Info", 867 | &cfg.server_url, 868 | &cfg.api_key, 869 | )) 870 | .expect("Unable to gather server information."); 871 | } 872 | Commands::ListLogs { 873 | json, 874 | output_format, 875 | } => { 876 | let logs = match get_log_filenames(ServerInfo::new( 877 | "/System/Logs", 878 | &cfg.server_url, 879 | &cfg.api_key, 880 | )) { 881 | Err(_) => { 882 | eprintln!("Unable to get get log filenames."); 883 | std::process::exit(1); 884 | } 885 | Ok(i) => i, 886 | }; 887 | 888 | if json { 889 | json_deprecation(); 890 | LogDetails::json_print(&logs); 891 | std::process::exit(0) 892 | } 893 | 894 | match output_format { 895 | OutputFormat::Json => { 896 | LogDetails::json_print(&logs); 897 | } 898 | OutputFormat::Csv => { 899 | LogDetails::csv_print(logs); 900 | } 901 | _ => { 902 | LogDetails::table_print(logs); 903 | } 904 | } 905 | } 906 | Commands::ShowLog { logfile } => { 907 | LogFile::get_logfile(LogFile::new( 908 | ServerInfo::new("/System/Logs/Log", &cfg.server_url, &cfg.api_key), 909 | logfile, 910 | )) 911 | .expect("Unable to retrieve the specified logfile."); 912 | } 913 | Commands::Reconfigure {} => { 914 | initial_config(cfg); 915 | } 916 | Commands::GetDevices { 917 | active, 918 | json, 919 | output_format, 920 | } => { 921 | let devices: Vec = match get_devices( 922 | ServerInfo::new(DEVICES, &cfg.server_url, &cfg.api_key), 923 | active, 924 | ) { 925 | Err(e) => { 926 | eprintln!("Unable to get devices, {e}"); 927 | std::process::exit(1); 928 | } 929 | Ok(i) => i, 930 | }; 931 | 932 | if json { 933 | json_deprecation(); 934 | DeviceDetails::json_print(&devices); 935 | std::process::exit(0) 936 | } 937 | 938 | match output_format { 939 | OutputFormat::Json => { 940 | DeviceDetails::json_print(&devices); 941 | } 942 | OutputFormat::Csv => { 943 | DeviceDetails::csv_print(&devices); 944 | } 945 | _ => { 946 | DeviceDetails::table_print(devices); 947 | } 948 | } 949 | } 950 | Commands::GetLibraries { 951 | json, 952 | output_format, 953 | } => { 954 | let libraries: Vec = match get_libraries(ServerInfo::new( 955 | "/Library/VirtualFolders", 956 | &cfg.server_url, 957 | &cfg.api_key, 958 | )) { 959 | Err(_) => { 960 | eprintln!("Unable to get libraries."); 961 | std::process::exit(1); 962 | } 963 | Ok(i) => i, 964 | }; 965 | 966 | if json { 967 | json_deprecation(); 968 | LibraryDetails::json_print(&libraries); 969 | std::process::exit(0) 970 | } 971 | 972 | match output_format { 973 | OutputFormat::Json => { 974 | LibraryDetails::json_print(&libraries); 975 | } 976 | OutputFormat::Csv => { 977 | LibraryDetails::csv_print(libraries); 978 | } 979 | _ => { 980 | LibraryDetails::table_print(libraries); 981 | } 982 | } 983 | } 984 | Commands::GetScheduledTasks { 985 | json, 986 | output_format, 987 | } => { 988 | let tasks: Vec = match get_scheduled_tasks(ServerInfo::new( 989 | "/ScheduledTasks", 990 | &cfg.server_url, 991 | &cfg.api_key, 992 | )) { 993 | Err(e) => { 994 | eprintln!("Unable to get scheduled tasks, {e}"); 995 | std::process::exit(1); 996 | } 997 | Ok(i) => i, 998 | }; 999 | 1000 | if json { 1001 | json_deprecation(); 1002 | TaskDetails::json_print(&tasks); 1003 | std::process::exit(0); 1004 | } 1005 | 1006 | match output_format { 1007 | OutputFormat::Json => { 1008 | TaskDetails::json_print(&tasks); 1009 | } 1010 | OutputFormat::Csv => { 1011 | TaskDetails::csv_print(&tasks); 1012 | } 1013 | _ => { 1014 | TaskDetails::table_print(tasks); 1015 | } 1016 | } 1017 | } 1018 | Commands::ExecuteTaskByName { task } => { 1019 | let taskid: String = match get_taskid_by_taskname( 1020 | ServerInfo::new("/ScheduledTasks", &cfg.server_url, &cfg.api_key), 1021 | &task, 1022 | ) { 1023 | Err(e) => { 1024 | eprintln!("Unable to get task id by taskname, {e}"); 1025 | std::process::exit(1); 1026 | } 1027 | Ok(i) => i, 1028 | }; 1029 | execute_task_by_id( 1030 | ServerInfo::new( 1031 | "/ScheduledTasks/Running/{taskId}", 1032 | &cfg.server_url, 1033 | &cfg.api_key, 1034 | ), 1035 | &task, 1036 | &taskid, 1037 | ); 1038 | } 1039 | Commands::ScanLibrary { 1040 | library_id, 1041 | scan_type, 1042 | } => { 1043 | if library_id == "all" { 1044 | scan_library_all(ServerInfo::new( 1045 | "/Library/Refresh", 1046 | &cfg.server_url, 1047 | &cfg.api_key, 1048 | )); 1049 | } else { 1050 | let query_info = match scan_type { 1051 | ScanType::NewUpdated => { 1052 | vec![ 1053 | ("Recursive", "true"), 1054 | ("ImageRefreshMode", "Default"), 1055 | ("MetadataRefreshMode", "Default"), 1056 | ("ReplaceAllImages", "false"), 1057 | ("RegenerateTrickplay", "false"), 1058 | ("ReplaceAllMetadata", "false"), 1059 | ] 1060 | } 1061 | ScanType::MissingMetadata => { 1062 | vec![ 1063 | ("Recursive", "true"), 1064 | ("ImageRefreshMode", "FullRefresh"), 1065 | ("MetadataRefreshMode", "FullRefresh"), 1066 | ("ReplaceAllImages", "false"), 1067 | ("RegenerateTrickplay", "false"), 1068 | ("ReplaceAllMetadata", "false"), 1069 | ] 1070 | } 1071 | ScanType::ReplaceMetadata => { 1072 | vec![ 1073 | ("Recursive", "true"), 1074 | ("ImageRefreshMode", "FullRefresh"), 1075 | ("MetadataRefreshMode", "FullRefresh"), 1076 | ("ReplaceAllImages", "false"), 1077 | ("RegenerateTrickplay", "false"), 1078 | ("ReplaceAllMetadata", "true"), 1079 | ] 1080 | } 1081 | _ => std::process::exit(1), 1082 | }; 1083 | scan_library( 1084 | ServerInfo::new("/Items/{library_id}/Refresh", &cfg.server_url, &cfg.api_key), 1085 | query_info, 1086 | library_id, 1087 | ); 1088 | } 1089 | } 1090 | Commands::RemoveDeviceByUsername { username } => { 1091 | let filtered: Vec = match get_deviceid_by_username( 1092 | ServerInfo::new(DEVICES, &cfg.server_url, &cfg.api_key), 1093 | &username, 1094 | ) { 1095 | Err(_) => { 1096 | eprintln!("Unable to get device id by username."); 1097 | std::process::exit(1); 1098 | } 1099 | Ok(i) => i, 1100 | }; 1101 | for item in filtered { 1102 | remove_device( 1103 | ServerInfo::new(DEVICES, &cfg.server_url, &cfg.api_key), 1104 | &item, 1105 | ) 1106 | .expect("Unable to delete specified id."); 1107 | } 1108 | } 1109 | Commands::RestartJellyfin {} => { 1110 | restart_or_shutdown(ServerInfo::new( 1111 | "/System/Restart", 1112 | &cfg.server_url, 1113 | &cfg.api_key, 1114 | )); 1115 | } 1116 | Commands::ShutdownJellyfin {} => { 1117 | restart_or_shutdown(ServerInfo::new( 1118 | "/System/Shutdown", 1119 | &cfg.server_url, 1120 | &cfg.api_key, 1121 | )); 1122 | } 1123 | Commands::GetPlugins { 1124 | json, 1125 | output_format, 1126 | } => { 1127 | let plugins: Vec = match PluginInfo::get_plugins(PluginInfo::new( 1128 | "/Plugins", 1129 | &cfg.server_url, 1130 | cfg.api_key, 1131 | )) { 1132 | Err(_) => { 1133 | eprintln!("Unable to get plugin information."); 1134 | std::process::exit(1); 1135 | } 1136 | Ok(i) => i, 1137 | }; 1138 | 1139 | if json { 1140 | json_deprecation(); 1141 | PluginDetails::json_print(&plugins); 1142 | std::process::exit(0) 1143 | } 1144 | 1145 | match output_format { 1146 | OutputFormat::Json => { 1147 | PluginDetails::json_print(&plugins); 1148 | } 1149 | OutputFormat::Csv => { 1150 | PluginDetails::csv_print(plugins); 1151 | } 1152 | _ => { 1153 | PluginDetails::table_print(plugins); 1154 | } 1155 | } 1156 | } 1157 | Commands::CreateReport { 1158 | report_type, 1159 | limit, 1160 | filename, 1161 | } => match report_type { 1162 | ReportType::Activity => { 1163 | println!("Gathering Activity information....."); 1164 | let activities: ActivityDetails = match get_activity( 1165 | ServerInfo::new("/System/ActivityLog/Entries", &cfg.server_url, &cfg.api_key), 1166 | &limit, 1167 | ) { 1168 | Err(e) => { 1169 | eprintln!("Unable to gather activity log entries, {e}"); 1170 | std::process::exit(1); 1171 | } 1172 | Ok(i) => i, 1173 | }; 1174 | if !filename.is_empty() { 1175 | println!("Exporting Activity information to {}.....", &filename); 1176 | let csv = ActivityDetails::print_as_csv(activities); 1177 | export_data(&csv, filename); 1178 | println!("Export complete."); 1179 | } else { 1180 | ActivityDetails::table_print(activities); 1181 | } 1182 | } 1183 | ReportType::Movie => { 1184 | let user_id: String = match UserList::get_current_user_information(UserList::new( 1185 | "/Users/Me", 1186 | &cfg.server_url, 1187 | &cfg.api_key, 1188 | )) { 1189 | Err(e) => { 1190 | eprintln!("Unable to gather information about current user, {e}"); 1191 | std::process::exit(1); 1192 | } 1193 | Ok(i) => i.id, 1194 | }; 1195 | let movies: MovieDetails = match export_library( 1196 | ServerInfo::new("/Users/{userId}/Items", &cfg.server_url, &cfg.api_key), 1197 | &user_id, 1198 | ) { 1199 | Err(e) => { 1200 | eprintln!("Unable to export library, {e}"); 1201 | std::process::exit(1); 1202 | } 1203 | Ok(i) => i, 1204 | }; 1205 | if !filename.is_empty() { 1206 | println!("Exporting Movie information to {}.....", &filename); 1207 | let csv = MovieDetails::print_as_csv(movies); 1208 | export_data(&csv, filename); 1209 | println!("Export complete."); 1210 | } else { 1211 | MovieDetails::table_print(movies); 1212 | } 1213 | } 1214 | }, 1215 | Commands::SearchMedia { 1216 | term, 1217 | mediatype, 1218 | parentid, 1219 | include_filepath, 1220 | output_format, 1221 | table_columns, 1222 | } => { 1223 | let search_result = execute_search(&term, mediatype, parentid, include_filepath, &cfg); 1224 | 1225 | let mut used_table_columns = table_columns.clone(); 1226 | 1227 | if include_filepath { 1228 | used_table_columns.push("Path".to_string()); 1229 | } 1230 | 1231 | match output_format { 1232 | OutputFormat::Json => { 1233 | MediaRoot::json_print(search_result); 1234 | } 1235 | OutputFormat::Csv => { 1236 | MediaRoot::csv_print(search_result, &used_table_columns); 1237 | } 1238 | _ => { 1239 | MediaRoot::table_print(search_result, &used_table_columns); 1240 | } 1241 | } 1242 | } 1243 | Commands::Completions { shell } => { 1244 | let cmd = &mut Cli::command(); 1245 | 1246 | generate(shell, cmd, cmd.get_name().to_string(), &mut io::stdout()); 1247 | } 1248 | } 1249 | 1250 | Ok(()) 1251 | } 1252 | 1253 | /// 1254 | /// JSON flag deprecation message. 1255 | /// 1256 | fn json_deprecation() { 1257 | println!("|========= DEPRECATION WARNING ============|"); 1258 | println!(" The \"--json\" flag has been deprecated."); 1259 | println!(" Please consider migrating to the"); 1260 | println!(" \"output_format\" argument"); 1261 | println!("|==========================================|"); 1262 | thread::sleep(time::Duration::from_millis(5000)); 1263 | } 1264 | 1265 | /// 1266 | /// Executes a search with the passed parameters. 1267 | /// 1268 | fn execute_search( 1269 | term: &str, 1270 | mediatype: String, 1271 | parentid: String, 1272 | include_filepath: bool, 1273 | cfg: &AppConfig, 1274 | ) -> MediaRoot { 1275 | let mut query = vec![ 1276 | ("SortBy", "SortName,ProductionYear"), 1277 | ("Recursive", "true"), 1278 | ("searchTerm", term), 1279 | ]; 1280 | if mediatype != "all" { 1281 | query.push(("IncludeItemTypes", &mediatype)); 1282 | } 1283 | 1284 | if include_filepath { 1285 | query.push(("fields", "Path")); 1286 | } 1287 | 1288 | if !parentid.is_empty() { 1289 | query.push(("parentId", &parentid)); 1290 | } 1291 | 1292 | match get_search_results( 1293 | ServerInfo::new("/Items", &cfg.server_url, &cfg.api_key), 1294 | query, 1295 | ) { 1296 | Err(e) => { 1297 | eprintln!("Unable to execute search, {e}"); 1298 | std::process::exit(1); 1299 | } 1300 | Ok(i) => i, 1301 | } 1302 | } 1303 | 1304 | /// 1305 | /// Retrieve the id for the specified user. Most API calls require the id of the user rather than the username. 1306 | /// 1307 | fn get_user_id(cfg: &AppConfig, username: &String) -> String { 1308 | UserList::get_user_id( 1309 | UserList::new("/Users", &cfg.server_url, &cfg.api_key), 1310 | username, 1311 | ) 1312 | } 1313 | 1314 | /// 1315 | /// Gathers user information. 1316 | /// 1317 | fn gather_user_information(cfg: &AppConfig, username: &String, id: &str) -> UserDetails { 1318 | match UserList::get_user_information(UserList::new(USER_ID, &cfg.server_url, &cfg.api_key), id) 1319 | { 1320 | Err(_) => { 1321 | println!("Unable to get user id for {}", username); 1322 | std::process::exit(1); 1323 | } 1324 | Ok(ul) => ul, 1325 | } 1326 | } 1327 | 1328 | /// 1329 | /// Helper function to standardize the call for adding a user with a password. 1330 | /// 1331 | fn add_user(cfg: &AppConfig, username: String, password: String) { 1332 | let server_path = format!("{}/Users/New", cfg.server_url); 1333 | match UserWithPass::create_user(UserWithPass::new( 1334 | Some(username), 1335 | Some(password), 1336 | None, 1337 | server_path, 1338 | cfg.api_key.clone(), 1339 | )) { 1340 | Err(_) => { 1341 | println!("Unable to create user"); 1342 | std::process::exit(1); 1343 | } 1344 | Ok(i) => i, 1345 | } 1346 | } 1347 | 1348 | /// 1349 | /// Executed on initial run or when user wants to redo configuration. Will attempt to auto-configure 1350 | /// the application prior to allowing customization by 1351 | /// the user. 1352 | /// 1353 | fn initial_config(mut cfg: AppConfig) { 1354 | println!("[INFO] Attempting to determine Jellyfin information....."); 1355 | env::consts::OS.clone_into(&mut cfg.os); 1356 | println!("[INFO] OS detected as {}.", cfg.os); 1357 | 1358 | print!("[INPUT] Please enter your Jellyfin URL: "); 1359 | io::stdout().flush().expect("Unable to get Jellyfin URL."); 1360 | let mut server_url_input = String::new(); 1361 | io::stdin() 1362 | .read_line(&mut server_url_input) 1363 | .expect("Could not read server url information"); 1364 | server_url_input.trim().clone_into(&mut cfg.server_url); 1365 | 1366 | print!("[INPUT] Please enter your Jellyfin username: "); 1367 | io::stdout().flush().expect("Unable to get username."); 1368 | let mut username = String::new(); 1369 | io::stdin() 1370 | .read_line(&mut username) 1371 | .expect("[ERROR] Could not read Jellyfin username"); 1372 | let password = rpassword::prompt_password("Please enter your Jellyfin password: ").unwrap(); 1373 | println!("[INFO] Attempting to authenticate user."); 1374 | cfg.api_key = UserAuth::auth_user(UserAuth::new(&cfg.server_url, username.trim(), password)) 1375 | .expect("Unable to generate user auth token. Please assure your configuration information was input correctly\n"); 1376 | 1377 | "configured".clone_into(&mut cfg.status); 1378 | token_to_api(cfg); 1379 | } 1380 | 1381 | /// 1382 | /// Due to an issue with api key processing in Jellyfin, JellyRoller was initially relied on using auto tokens to communicate. 1383 | /// Now that the issue has been fixed, the auto tokens need to be converted to an API key. The single purpose of this function 1384 | /// is to handle the conversion with no input required from the user. 1385 | /// 1386 | fn token_to_api(mut cfg: AppConfig) { 1387 | println!("[INFO] Attempting to auto convert user auth token to API key....."); 1388 | // Check if api key already exists 1389 | if UserWithPass::retrieve_api_token(UserWithPass::new( 1390 | None, 1391 | None, 1392 | None, 1393 | format!("{}/Auth/Keys", cfg.server_url), 1394 | cfg.api_key.clone(), 1395 | )) 1396 | .unwrap() 1397 | .is_empty() 1398 | { 1399 | UserWithPass::create_api_token(UserWithPass::new( 1400 | None, 1401 | None, 1402 | None, 1403 | format!("{}/Auth/Keys", cfg.server_url), 1404 | cfg.api_key.clone(), 1405 | )); 1406 | } 1407 | cfg.api_key = UserWithPass::retrieve_api_token(UserWithPass::new( 1408 | None, 1409 | None, 1410 | None, 1411 | format!("{}/Auth/Keys", cfg.server_url), 1412 | cfg.api_key, 1413 | )) 1414 | .unwrap(); 1415 | cfg.token = "apiKey".to_string(); 1416 | confy::store("jellyroller", "jellyroller", cfg) 1417 | .expect("[ERROR] Unable to store updated configuration."); 1418 | println!("[INFO] Auth token successfully converted to API key."); 1419 | } 1420 | 1421 | /// 1422 | /// Function that converts an image into a base64 png image. 1423 | /// 1424 | fn image_to_base64(path: String) -> String { 1425 | let base_img = image::open(path).unwrap(); 1426 | let mut image_data: Vec = Vec::new(); 1427 | base_img 1428 | .write_to(&mut Cursor::new(&mut image_data), ImageFormat::Png) 1429 | .unwrap(); 1430 | general_purpose::STANDARD.encode(image_data) 1431 | } 1432 | 1433 | /// 1434 | /// Custom implementation to convert the ImageType enum into Strings 1435 | /// for easy comparison. 1436 | /// 1437 | impl fmt::Display for ImageType { 1438 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 1439 | match self { 1440 | ImageType::Primary => write!(f, "Primary"), 1441 | ImageType::Art => write!(f, "Art"), 1442 | ImageType::Backdrop => write!(f, "Backdrop"), 1443 | ImageType::Banner => write!(f, "Banner"), 1444 | ImageType::Logo => write!(f, "Logo"), 1445 | ImageType::Thumb => write!(f, "Thumb"), 1446 | ImageType::Disc => write!(f, "Disc"), 1447 | ImageType::Box => write!(f, "Box"), 1448 | ImageType::Screenshot => write!(f, "Screenshot"), 1449 | ImageType::Menu => write!(f, "Menu"), 1450 | ImageType::BoxRear => write!(f, "BoxRear"), 1451 | ImageType::Profile => write!(f, "Profile"), 1452 | } 1453 | } 1454 | } 1455 | 1456 | /// 1457 | /// Custom implementation to convert collectiontype enum into Strings 1458 | /// 1459 | impl fmt::Display for CollectionType { 1460 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 1461 | match self { 1462 | CollectionType::Movies => write!(f, "movies"), 1463 | CollectionType::TVShows => write!(f, "tvshows"), 1464 | CollectionType::Music => write!(f, "music"), 1465 | CollectionType::MusicVideos => write!(f, "musicvideos"), 1466 | CollectionType::HomeVideos => write!(f, "homevideos"), 1467 | CollectionType::BoxSets => write!(f, "boxsets"), 1468 | CollectionType::Books => write!(f, "books"), 1469 | CollectionType::Mixed => write!(f, "mixed"), 1470 | } 1471 | } 1472 | } 1473 | -------------------------------------------------------------------------------- /src/plugin_actions.rs: -------------------------------------------------------------------------------- 1 | use super::{ 2 | handle_others, handle_unauthorized, responder::simple_get, PluginDetails, PluginRootJson, 3 | }; 4 | use reqwest::StatusCode; 5 | 6 | #[derive(Clone)] 7 | pub struct PluginInfo { 8 | server_url: String, 9 | api_key: String, 10 | } 11 | 12 | impl PluginInfo { 13 | pub fn new(endpoint: &str, server_url: &str, api_key: String) -> PluginInfo { 14 | PluginInfo { 15 | server_url: format!("{}{}", server_url, endpoint), 16 | api_key, 17 | } 18 | } 19 | 20 | pub fn get_plugins(self) -> Result, Box> { 21 | let response = simple_get(self.server_url, self.api_key, Vec::new()); 22 | match response.status() { 23 | StatusCode::OK => { 24 | let json = response.text()?; 25 | let plugins = serde_json::from_str::(&json)?; 26 | return Ok(plugins); 27 | } 28 | StatusCode::UNAUTHORIZED => { 29 | handle_unauthorized(); 30 | } 31 | _ => { 32 | handle_others(response); 33 | } 34 | } 35 | 36 | Ok(Vec::new()) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/responder.rs: -------------------------------------------------------------------------------- 1 | use reqwest::{blocking::Client, blocking::Response, header::CONTENT_TYPE}; 2 | 3 | pub fn simple_get(server_url: String, api_key: String, query: Vec<(&str, &str)>) -> Response { 4 | let client = Client::new(); 5 | let response = client 6 | .get(server_url) 7 | .header("Authorization", format!("MediaBrowser Token=\"{api_key}\"")) 8 | .query(&query) 9 | .send(); 10 | if let Ok(resp) = response { 11 | resp 12 | } else { 13 | println!("Get response error."); 14 | std::process::exit(1); 15 | } 16 | } 17 | 18 | pub fn simple_post(server_url: String, api_key: String, body: String) -> Response { 19 | let client = Client::new(); 20 | let response = client 21 | .post(server_url) 22 | .header(CONTENT_TYPE, "application/json") 23 | .header("Authorization", format!("MediaBrowser Token=\"{api_key}\"")) 24 | .body(body) 25 | .send(); 26 | if let Ok(resp) = response { 27 | resp 28 | } else { 29 | println!("Post response error."); 30 | std::process::exit(1); 31 | } 32 | } 33 | 34 | pub fn simple_post_with_query( 35 | server_url: String, 36 | api_key: String, 37 | body: String, 38 | query: Vec<(&str, &str)>, 39 | ) -> Response { 40 | let client = Client::new(); 41 | let response = client 42 | .post(server_url) 43 | .header(CONTENT_TYPE, "application/json") 44 | .header("Authorization", format!("MediaBrowser Token=\"{api_key}\"")) 45 | .body(body) 46 | .query(&query) 47 | .send(); 48 | if let Ok(resp) = response { 49 | resp 50 | } else { 51 | println!("Post with query response error."); 52 | std::process::exit(1); 53 | } 54 | } 55 | 56 | pub fn simple_post_image(server_url: String, api_key: String, body: String) -> Response { 57 | let client = Client::new(); 58 | let response = client 59 | .post(server_url) 60 | .header(CONTENT_TYPE, "image/png") 61 | .header("Authorization", format!("MediaBrowser Token=\"{api_key}\"")) 62 | .body(body) 63 | .send(); 64 | if let Ok(resp) = response { 65 | resp 66 | } else { 67 | println!("Post image response error."); 68 | std::process::exit(1); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/system_actions.rs: -------------------------------------------------------------------------------- 1 | use crate::entities::{ 2 | activity_details::ActivityDetails, media_details::MediaRoot, 3 | repository_details::RepositoryDetails, task_details::TaskDetails, 4 | }; 5 | 6 | use super::{ 7 | handle_others, handle_unauthorized, 8 | responder::{simple_get, simple_post, simple_post_image, simple_post_with_query}, 9 | DeviceDetails, DeviceRootJson, ImageType, LibraryDetails, LibraryRootJson, LogDetails, 10 | MovieDetails, PackageDetails, PackageDetailsRoot, RepositoryDetailsRoot, ServerInfo, 11 | }; 12 | use chrono::{DateTime, Duration}; 13 | use reqwest::{blocking::Client, StatusCode}; 14 | use serde_json::Value; 15 | 16 | pub type LogFileVec = Vec; 17 | pub type ScheduledTasksVec = Vec; 18 | 19 | // Currently used for server-info, restart-jellyfin, shutdown-jellyfin 20 | pub fn get_server_info(server_info: ServerInfo) -> Result<(), Box> { 21 | let response = simple_get(server_info.server_url, server_info.api_key, Vec::new()); 22 | match response.status() { 23 | StatusCode::OK => { 24 | let body: Value = response.json()?; 25 | println!("{:#}", body); 26 | } 27 | StatusCode::UNAUTHORIZED => { 28 | handle_unauthorized(); 29 | } 30 | _ => { 31 | handle_others(response); 32 | } 33 | } 34 | 35 | Ok(()) 36 | } 37 | 38 | pub fn get_repo_info( 39 | server_info: ServerInfo, 40 | ) -> Result, Box> { 41 | let response = simple_get(server_info.server_url, server_info.api_key, Vec::new()); 42 | let mut repos = Vec::new(); 43 | match response.status() { 44 | StatusCode::OK => { 45 | repos = response.json::()?; 46 | } 47 | _ => handle_others(response), 48 | } 49 | Ok(repos) 50 | } 51 | 52 | pub fn set_repo_info(server_info: ServerInfo, repos: Vec) { 53 | simple_post( 54 | server_info.server_url, 55 | server_info.api_key, 56 | serde_json::to_string(&repos).unwrap(), 57 | ); 58 | } 59 | 60 | pub fn get_packages_info( 61 | server_info: ServerInfo, 62 | ) -> Result, Box> { 63 | let response = simple_get(server_info.server_url, server_info.api_key, Vec::new()); 64 | let mut packages = Vec::new(); 65 | match response.status() { 66 | StatusCode::OK => { 67 | packages = response.json::()?; 68 | } 69 | _ => handle_others(response), 70 | } 71 | Ok(packages) 72 | } 73 | 74 | pub fn install_package(server_info: ServerInfo, package: &str, version: &str, repository: &str) { 75 | let query = vec![("version", version), ("repository", repository)]; 76 | let response = simple_post_with_query( 77 | server_info.server_url.replace("{package}", package), 78 | server_info.api_key, 79 | String::new(), 80 | query, 81 | ); 82 | match response.status() { 83 | StatusCode::NO_CONTENT => { 84 | println!("Package successfully installed."); 85 | } 86 | StatusCode::UNAUTHORIZED => { 87 | handle_unauthorized(); 88 | } 89 | _ => { 90 | handle_others(response); 91 | } 92 | } 93 | } 94 | 95 | pub fn return_server_info(server_info: ServerInfo) -> String { 96 | let response = simple_get(server_info.server_url, server_info.api_key, Vec::new()); 97 | match response.status() { 98 | StatusCode::OK => { 99 | let body: Value = response.json().unwrap(); 100 | body.to_string() 101 | } 102 | _ => { 103 | handle_others(response); 104 | "".to_string() 105 | } 106 | } 107 | } 108 | 109 | pub fn restart_or_shutdown(server_info: ServerInfo) { 110 | let response = simple_post(server_info.server_url, server_info.api_key, String::new()); 111 | match response.status() { 112 | StatusCode::NO_CONTENT => { 113 | println!("Command successful."); 114 | } 115 | StatusCode::UNAUTHORIZED => { 116 | handle_unauthorized(); 117 | } 118 | _ => { 119 | handle_others(response); 120 | } 121 | } 122 | } 123 | 124 | pub fn get_log_filenames( 125 | server_info: ServerInfo, 126 | ) -> Result, Box> { 127 | let response = simple_get(server_info.server_url, server_info.api_key, Vec::new()); 128 | let mut details = Vec::new(); 129 | match response.status() { 130 | StatusCode::OK => { 131 | let logs = response.json::()?; 132 | for log in logs { 133 | details.push(LogDetails::new( 134 | log.date_created, 135 | log.date_modified, 136 | log.name, 137 | log.size / 1024, 138 | )); 139 | } 140 | } 141 | StatusCode::UNAUTHORIZED => { 142 | handle_unauthorized(); 143 | } 144 | _ => { 145 | handle_others(response); 146 | } 147 | } 148 | 149 | Ok(details) 150 | } 151 | 152 | pub fn get_devices( 153 | server_info: ServerInfo, 154 | active: bool, 155 | ) -> Result, Box> { 156 | let response = simple_get(server_info.server_url, server_info.api_key, Vec::new()); 157 | let mut details = Vec::new(); 158 | match response.status() { 159 | StatusCode::OK => { 160 | let json = response.text()?; 161 | let devices = serde_json::from_str::(&json)?; 162 | let cutofftime = chrono::offset::Utc::now() - Duration::seconds(960); 163 | for device in devices.items { 164 | let datetime = DateTime::parse_from_rfc3339(&device.lastactivity).unwrap(); 165 | if active { 166 | if cutofftime < datetime { 167 | details.push(DeviceDetails::new( 168 | device.id, 169 | device.name, 170 | device.lastusername, 171 | device.lastactivity, 172 | )); 173 | } 174 | } else { 175 | details.push(DeviceDetails::new( 176 | device.id, 177 | device.name, 178 | device.lastusername, 179 | device.lastactivity, 180 | )); 181 | } 182 | } 183 | } 184 | StatusCode::UNAUTHORIZED => { 185 | handle_unauthorized(); 186 | } 187 | _ => { 188 | handle_others(response); 189 | } 190 | } 191 | 192 | Ok(details) 193 | } 194 | 195 | pub fn get_libraries( 196 | server_info: ServerInfo, 197 | ) -> Result, Box> { 198 | let response = simple_get(server_info.server_url, server_info.api_key, Vec::new()); 199 | let mut details = Vec::new(); 200 | match response.status() { 201 | StatusCode::OK => { 202 | let json = response.text()?; 203 | let libraries = serde_json::from_str::(&json)?; 204 | for library in libraries { 205 | details.push(LibraryDetails::new( 206 | library.name, 207 | library.collection_type, 208 | library.item_id, 209 | library.refresh_status, 210 | )); 211 | } 212 | } 213 | StatusCode::UNAUTHORIZED => { 214 | handle_unauthorized(); 215 | } 216 | _ => { 217 | handle_others(response); 218 | } 219 | } 220 | Ok(details) 221 | } 222 | 223 | pub fn export_library( 224 | server_info: ServerInfo, 225 | user_id: &str, 226 | ) -> Result> { 227 | let query = vec![ 228 | ("SortBy", "SortName,ProductionYear"), 229 | ("IncludeItemTypes", "Movie"), 230 | ("Recursive", "true"), 231 | ("fields", "Genres,DateCreated,Width,Height,Path"), 232 | ]; 233 | let response = simple_get( 234 | server_info.server_url.replace("{userId}", user_id), 235 | server_info.api_key, 236 | query, 237 | ); 238 | match response.status() { 239 | StatusCode::OK => { 240 | let details = response.json::()?; 241 | Ok(details) 242 | } 243 | _ => { 244 | handle_others(response); 245 | std::process::exit(1) 246 | } 247 | } 248 | } 249 | 250 | pub fn get_activity( 251 | server_info: ServerInfo, 252 | limit: &str, 253 | ) -> Result> { 254 | let query = vec![("limit", limit)]; 255 | let response = simple_get(server_info.server_url, server_info.api_key, query); 256 | match response.status() { 257 | StatusCode::OK => { 258 | let activities = response.json::()?; 259 | Ok(activities) 260 | } 261 | _ => { 262 | handle_others(response); 263 | std::process::exit(1); 264 | } 265 | } 266 | } 267 | 268 | pub fn get_taskid_by_taskname( 269 | server_info: ServerInfo, 270 | taskname: &str, 271 | ) -> Result> { 272 | let response = simple_get(server_info.server_url, server_info.api_key, Vec::new()); 273 | match response.status() { 274 | StatusCode::OK => { 275 | let tasks = response.json::()?; 276 | for task in tasks { 277 | if task.name.to_lowercase() == taskname.to_lowercase() { 278 | return Ok(task.id); 279 | } 280 | } 281 | } 282 | StatusCode::UNAUTHORIZED => { 283 | handle_unauthorized(); 284 | } 285 | _ => { 286 | handle_others(response); 287 | } 288 | } 289 | Ok(String::new()) 290 | } 291 | 292 | pub fn execute_task_by_id(server_info: ServerInfo, taskname: &str, taskid: &str) { 293 | let response = simple_post( 294 | server_info.server_url.replace("{taskId}", taskid), 295 | server_info.api_key, 296 | String::new(), 297 | ); 298 | match response.status() { 299 | StatusCode::NO_CONTENT => { 300 | println!("Task \"{}\" initiated.", taskname); 301 | } 302 | StatusCode::UNAUTHORIZED => { 303 | handle_unauthorized(); 304 | } 305 | _ => { 306 | handle_others(response); 307 | } 308 | } 309 | } 310 | 311 | pub fn get_deviceid_by_username( 312 | server_info: ServerInfo, 313 | username: &str, 314 | ) -> Result, Box> { 315 | let response = simple_get(server_info.server_url, server_info.api_key, Vec::new()); 316 | let mut filtered = Vec::new(); 317 | match response.status() { 318 | StatusCode::OK => { 319 | let json = response.text()?; 320 | let devices = serde_json::from_str::(&json)?; 321 | for device in devices.items { 322 | if device.lastusername == username { 323 | filtered.push(device.id); 324 | } 325 | } 326 | } 327 | StatusCode::UNAUTHORIZED => { 328 | handle_unauthorized(); 329 | } 330 | _ => { 331 | handle_others(response); 332 | } 333 | } 334 | 335 | Ok(filtered) 336 | } 337 | 338 | pub fn remove_device(server_info: ServerInfo, id: &str) -> Result<(), reqwest::Error> { 339 | let client = Client::new(); 340 | let apikey = server_info.api_key; 341 | let response = client 342 | .delete(server_info.server_url) 343 | .header("Authorization", format!("MediaBrowser Token=\"{apikey}\"")) 344 | .query(&[("id", &id)]) 345 | .send()?; 346 | match response.status() { 347 | StatusCode::NO_CONTENT => { 348 | println!("\t Removes device with id = {}.", id); 349 | } 350 | StatusCode::UNAUTHORIZED => { 351 | handle_unauthorized(); 352 | } 353 | _ => { 354 | handle_others(response); 355 | } 356 | } 357 | Ok(()) 358 | } 359 | 360 | pub fn get_scheduled_tasks(server_info: ServerInfo) -> Result, reqwest::Error> { 361 | let response = simple_get(server_info.server_url, server_info.api_key, Vec::new()); 362 | let mut details = Vec::new(); 363 | match response.status() { 364 | StatusCode::OK => { 365 | let scheduled_tasks = response.json::()?; 366 | for task in scheduled_tasks { 367 | details.push(TaskDetails::new( 368 | task.name, 369 | task.state, 370 | task.percent_complete, 371 | task.id, 372 | )); 373 | } 374 | } 375 | StatusCode::UNAUTHORIZED => { 376 | handle_unauthorized(); 377 | } 378 | _ => { 379 | handle_others(response); 380 | } 381 | } 382 | 383 | Ok(details) 384 | } 385 | 386 | pub fn scan_library(server_info: ServerInfo, scan_options: Vec<(&str, &str)>, library_id: String) { 387 | let response = simple_post_with_query( 388 | server_info 389 | .server_url 390 | .replace("{library_id}", library_id.as_str()), 391 | server_info.api_key, 392 | String::new(), 393 | scan_options, 394 | ); 395 | match response.status() { 396 | StatusCode::NO_CONTENT => { 397 | println!("Library scan initiated."); 398 | } 399 | StatusCode::UNAUTHORIZED => { 400 | handle_unauthorized(); 401 | } 402 | _ => { 403 | handle_others(response); 404 | } 405 | } 406 | } 407 | 408 | pub fn scan_library_all(server_info: ServerInfo) { 409 | let response = simple_post(server_info.server_url, server_info.api_key, String::new()); 410 | match response.status() { 411 | StatusCode::NO_CONTENT => { 412 | println!("Library scan initiated."); 413 | } 414 | StatusCode::UNAUTHORIZED => { 415 | handle_unauthorized(); 416 | } 417 | _ => { 418 | handle_others(response); 419 | } 420 | } 421 | } 422 | 423 | pub fn register_library(server_info: ServerInfo, json_contents: String) { 424 | let response = simple_post(server_info.server_url, server_info.api_key, json_contents); 425 | match response.status() { 426 | StatusCode::NO_CONTENT => { 427 | println!("Library successfully added."); 428 | } 429 | StatusCode::UNAUTHORIZED => { 430 | handle_unauthorized(); 431 | } 432 | _ => { 433 | handle_others(response); 434 | } 435 | } 436 | } 437 | 438 | pub fn update_image( 439 | server_info: ServerInfo, 440 | id: String, 441 | imagetype: &ImageType, 442 | img_base64: &String, 443 | ) { 444 | let response = simple_post_image( 445 | server_info 446 | .server_url 447 | .replace("{itemId}", id.as_str()) 448 | .replace("{imageType}", imagetype.to_string().as_str()), 449 | server_info.api_key, 450 | img_base64.to_string(), 451 | ); 452 | match response.status() { 453 | StatusCode::NO_CONTENT => { 454 | println!("Image successfully updated."); 455 | } 456 | StatusCode::UNAUTHORIZED => { 457 | handle_unauthorized(); 458 | } 459 | _ => { 460 | handle_others(response); 461 | } 462 | } 463 | } 464 | 465 | pub fn update_metadata(server_info: ServerInfo, id: String, json: String) { 466 | let response = simple_post( 467 | server_info.server_url.replace("{itemId}", id.as_str()), 468 | server_info.api_key, 469 | json, 470 | ); 471 | match response.status() { 472 | StatusCode::NO_CONTENT => { 473 | println!("Metadata successfully updated."); 474 | } 475 | StatusCode::UNAUTHORIZED => { 476 | handle_unauthorized(); 477 | } 478 | _ => { 479 | handle_others(response); 480 | } 481 | } 482 | } 483 | 484 | pub fn get_search_results( 485 | server_info: ServerInfo, 486 | query: Vec<(&str, &str)>, 487 | ) -> Result> { 488 | let response = simple_get(server_info.server_url, server_info.api_key, query); 489 | match response.status() { 490 | StatusCode::OK => { 491 | let media = response.json::()?; 492 | Ok(media) 493 | } 494 | StatusCode::UNAUTHORIZED => { 495 | handle_unauthorized(); 496 | std::process::exit(1); 497 | } 498 | _ => { 499 | handle_others(response); 500 | std::process::exit(1); 501 | } 502 | } 503 | } 504 | pub struct LogFile { 505 | server_info: ServerInfo, 506 | logname: String, 507 | } 508 | 509 | impl LogFile { 510 | pub fn new(server_info: ServerInfo, logname: String) -> LogFile { 511 | LogFile { 512 | server_info, 513 | logname, 514 | } 515 | } 516 | 517 | pub fn get_logfile(self) -> Result<(), reqwest::Error> { 518 | 519 | let client = Client::new(); 520 | let apikey = self.server_info.api_key; 521 | let response = client 522 | .get(self.server_info.server_url) 523 | .query(&[("name", self.logname)]) 524 | .header("Authorization", format!("MediaBrowser Token=\"{apikey}\"")) 525 | .send()?; 526 | match response.status() { 527 | StatusCode::OK => { 528 | let body = response.text(); 529 | println!("{:#}", body?); 530 | } 531 | StatusCode::UNAUTHORIZED => { 532 | handle_unauthorized(); 533 | } 534 | _ => { 535 | handle_others(response); 536 | } 537 | } 538 | Ok(()) 539 | } 540 | } 541 | -------------------------------------------------------------------------------- /src/user_actions.rs: -------------------------------------------------------------------------------- 1 | use crate::entities::token_details::TokenDetails; 2 | 3 | use super::{ 4 | handle_others, handle_unauthorized, 5 | responder::{simple_get, simple_post}, 6 | Policy, UserDetails, 7 | }; 8 | use reqwest::{ 9 | blocking::Client, 10 | header::{CONTENT_LENGTH, CONTENT_TYPE}, 11 | StatusCode, 12 | }; 13 | 14 | #[derive(Serialize, Deserialize)] 15 | pub struct UserWithPass { 16 | #[serde(rename = "Name")] 17 | username: Option, 18 | #[serde(rename = "NewPw")] 19 | pass: Option, 20 | #[serde(rename = "CurrentPw")] 21 | currentpwd: Option, 22 | server_url: String, 23 | auth_key: String, 24 | } 25 | 26 | impl UserWithPass { 27 | pub fn new( 28 | username: Option, 29 | pass: Option, 30 | currentpwd: Option, 31 | server_url: String, 32 | auth_key: String, 33 | ) -> UserWithPass { 34 | UserWithPass { 35 | username: Some(username.unwrap_or_else(|| String::new())), 36 | pass: Some(pass.unwrap_or_else(|| String::new())), 37 | currentpwd: Some(currentpwd.unwrap_or_else(|| String::new())), 38 | server_url, 39 | auth_key, 40 | } 41 | } 42 | 43 | pub fn resetpass(self) -> Result<(), Box> { 44 | let response = simple_post( 45 | self.server_url.clone(), 46 | self.auth_key.clone(), 47 | serde_json::to_string_pretty(&self)?, 48 | ); 49 | match response.status() { 50 | StatusCode::NO_CONTENT => { 51 | println!("Password updated successfully."); 52 | } 53 | StatusCode::UNAUTHORIZED => { 54 | handle_unauthorized(); 55 | } 56 | _ => { 57 | println!("{}", response.status()); 58 | handle_others(response); 59 | } 60 | } 61 | 62 | Ok(()) 63 | } 64 | 65 | pub fn create_user(self) -> Result<(), Box> { 66 | let response = simple_post( 67 | self.server_url.clone(), 68 | self.auth_key.clone(), 69 | serde_json::to_string_pretty(&self)?, 70 | ); 71 | match response.status() { 72 | StatusCode::OK => { 73 | println!("User \"{}\" successfully created.", &self.username.unwrap()); 74 | } 75 | StatusCode::UNAUTHORIZED => { 76 | handle_unauthorized(); 77 | } 78 | _ => { 79 | handle_others(response); 80 | } 81 | } 82 | 83 | Ok(()) 84 | } 85 | 86 | pub fn delete_user(self) -> Result<(), Box> { 87 | let client = reqwest::blocking::Client::new(); 88 | let apikey = self.auth_key; 89 | let response = client 90 | .delete(self.server_url) 91 | .header("Authorization", format!("MediaBrowser Token=\"{apikey}\"")) 92 | .header(CONTENT_TYPE, "application/json") 93 | .send()?; 94 | match response.status() { 95 | StatusCode::NO_CONTENT => { 96 | println!("User \"{}\" successfully removed.", &self.username.unwrap()); 97 | } 98 | StatusCode::UNAUTHORIZED => { 99 | handle_unauthorized(); 100 | } 101 | _ => { 102 | handle_others(response); 103 | } 104 | } 105 | 106 | Ok(()) 107 | } 108 | 109 | pub fn create_api_token(self) { 110 | let client = Client::new(); 111 | let apikey = self.auth_key; 112 | let response = client 113 | .post(self.server_url) 114 | .header("Authorization", format!("MediaBrowser Token=\"{apikey}\"")) 115 | .header(CONTENT_LENGTH, 0) 116 | .query(&[("app", "JellyRoller")]) 117 | .send() 118 | .unwrap(); 119 | 120 | match response.status() { 121 | StatusCode::NO_CONTENT => { 122 | println!("API key created."); 123 | } 124 | _ => { 125 | handle_others(response); 126 | } 127 | } 128 | } 129 | 130 | pub fn retrieve_api_token(self) -> Result> { 131 | let response = simple_get(self.server_url, self.auth_key, Vec::new()); 132 | match response.status() { 133 | StatusCode::OK => { 134 | let tokens = serde_json::from_str::(&response.text()?)?; 135 | for token in tokens.items { 136 | if token.app_name == "JellyRoller" { 137 | return Ok(token.access_token); 138 | } 139 | } 140 | } 141 | StatusCode::UNAUTHORIZED => { 142 | handle_unauthorized(); 143 | } 144 | _ => { 145 | handle_others(response); 146 | } 147 | } 148 | Ok(String::new()) 149 | } 150 | } 151 | 152 | #[derive(Serialize, Deserialize)] 153 | #[serde(rename_all = "camelCase")] 154 | pub struct UserAuthJson { 155 | #[serde(rename = "AccessToken")] 156 | pub access_token: String, 157 | #[serde(rename = "ServerId")] 158 | pub server_id: String, 159 | } 160 | 161 | pub type UserInfoVec = Vec; 162 | 163 | #[derive(Serialize, Deserialize)] 164 | pub struct UserAuth { 165 | server_url: String, 166 | username: String, 167 | pw: String, 168 | } 169 | 170 | impl UserAuth { 171 | pub fn new(server_url: &str, username: &str, password: String) -> UserAuth { 172 | UserAuth { 173 | server_url: format!("{}/Users/authenticatebyname", server_url), 174 | username: username.to_owned(), 175 | pw: password, 176 | } 177 | } 178 | 179 | pub fn auth_user(self) -> Result> { 180 | let client = Client::new(); 181 | let response = client 182 | .post(self.server_url.clone()) 183 | .header(CONTENT_TYPE, "application/json") 184 | .header("Authorization", "MediaBrowser Client=\"JellyRoller\", Device=\"jellyroller\", DeviceId=\"1\", Version=\"0.0.1\"") 185 | .body(serde_json::to_string_pretty(&self)?) 186 | .send()?; 187 | match response.status() { 188 | StatusCode::OK => { 189 | let result = response.json::()?; 190 | println!("[INFO] User authenticated successfully."); 191 | Ok(result.access_token) 192 | } 193 | _ => { 194 | // Panic since the application requires an authenticated user 195 | handle_others(response); 196 | panic!("[ERROR] Unable to authenticate user. Please assure your configuration information is correct.\n"); 197 | } 198 | } 199 | } 200 | } 201 | 202 | #[derive(Clone)] 203 | pub struct UserList { 204 | server_url: String, 205 | api_key: String, 206 | } 207 | 208 | impl UserList { 209 | pub fn new(endpoint: &str, server_url: &str, api_key: &str) -> UserList { 210 | UserList { 211 | server_url: format!("{}{}", server_url, endpoint), 212 | api_key: api_key.to_string(), 213 | } 214 | } 215 | 216 | pub fn list_users(self) -> Result, Box> { 217 | let response = simple_get(self.server_url, self.api_key, Vec::new()); 218 | let mut users = Vec::new(); 219 | match response.status() { 220 | StatusCode::OK => { 221 | users = response.json::()?; 222 | } 223 | StatusCode::UNAUTHORIZED => { 224 | handle_unauthorized(); 225 | } 226 | _ => { 227 | handle_others(response); 228 | } 229 | } 230 | 231 | Ok(users) 232 | } 233 | 234 | // TODO: Standardize the GET request? 235 | pub fn get_user_id(self, username: &String) -> String { 236 | let response = simple_get(self.server_url, self.api_key, Vec::new()); 237 | let users = response.json::().unwrap(); 238 | for user in users { 239 | if user.name == *username { 240 | return user.id; 241 | } 242 | } 243 | 244 | // Supplied username could not be found. Panic. 245 | panic!("Could not find user {}.", username); 246 | } 247 | 248 | pub fn get_user_information(self, id: &str) -> Result> { 249 | let response = simple_get( 250 | self.server_url.replace("{userId}", id), 251 | self.api_key, 252 | Vec::new(), 253 | ); 254 | Ok(serde_json::from_str(response.text()?.as_str())?) 255 | } 256 | 257 | pub fn get_current_user_information(self) -> Result> { 258 | let response = simple_get(self.server_url, self.api_key, Vec::new()); 259 | Ok(response.json::()?) 260 | } 261 | 262 | pub fn update_user_config_bool( 263 | self, 264 | user_info: &Policy, 265 | id: &str, 266 | username: &str, 267 | ) -> Result<(), Box> { 268 | let body = serde_json::to_string_pretty(user_info)?; 269 | let response = simple_post( 270 | self.server_url.replace("{userId}", id), 271 | self.api_key.clone(), 272 | body, 273 | ); 274 | if response.status() == StatusCode::NO_CONTENT { 275 | println!("User {} successfully updated.", username); 276 | } else { 277 | println!("Unable to update user policy information."); 278 | println!("Status Code: {}", response.status()); 279 | println!("{}", response.text()?); 280 | } 281 | Ok(()) 282 | } 283 | 284 | // 285 | // I really hate this function but it works for now. 286 | // 287 | pub fn update_user_info( 288 | self, 289 | id: &str, 290 | info: &UserDetails, 291 | ) -> Result<(), Box> { 292 | let body = serde_json::to_string_pretty(&info)?; 293 | // So we have to update the Policy and the user info separate even though they are the same JSON object :/ 294 | 295 | // First we will update the Policy 296 | let policy_url = format!("{}/Policy", self.server_url); 297 | let user_response = simple_post( 298 | self.server_url.replace("{userId}", id), 299 | self.api_key.clone(), 300 | body, 301 | ); 302 | if user_response.status() == StatusCode::NO_CONTENT { 303 | } else { 304 | println!("Unable to update user information."); 305 | println!("Status Code: {}", user_response.status()); 306 | match user_response.text() { 307 | Ok(t) => println!("{}", t), 308 | Err(_) => eprintln!("Could not get response text from user information update."), 309 | } 310 | } 311 | 312 | let response = simple_post( 313 | policy_url.replace("{userId}", id), 314 | self.api_key, 315 | serde_json::to_string_pretty(&info.policy)?, 316 | ); 317 | if response.status() == StatusCode::NO_CONTENT { 318 | println!("{} successfully updated.", info.name); 319 | } else { 320 | println!("Unable to update user information."); 321 | println!("Status Code: {}", response.status()); 322 | match response.text() { 323 | Ok(t) => println!("{}", t), 324 | Err(_) => eprintln!("Could not get response text from user policy update."), 325 | } 326 | } 327 | 328 | Ok(()) 329 | } 330 | } 331 | -------------------------------------------------------------------------------- /src/utils/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod output_writer; 2 | pub mod status_handler; 3 | -------------------------------------------------------------------------------- /src/utils/output_writer.rs: -------------------------------------------------------------------------------- 1 | use std::fs::File; 2 | use std::io::{BufWriter, Write}; 3 | 4 | pub fn export_data(data: &str, path: String) { 5 | let f = File::create(path).expect("Unable to create file"); 6 | let mut f = BufWriter::new(f); 7 | f.write_all(data.as_bytes()).expect("Unable to write data"); 8 | } 9 | -------------------------------------------------------------------------------- /src/utils/status_handler.rs: -------------------------------------------------------------------------------- 1 | use reqwest::blocking::Response; 2 | 3 | pub fn handle_unauthorized() { 4 | println!("Authentication failed. Try reconfiguring with \"jellyroller reconfigure\""); 5 | std::process::exit(1); 6 | } 7 | 8 | pub fn handle_others(response: Response) { 9 | println!("Status Code: {}", response.status()); 10 | std::process::exit(1); 11 | } 12 | --------------------------------------------------------------------------------