├── .github └── workflows │ └── rust.yml ├── .gitignore ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md └── src └── lib.rs /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Build 20 | run: cargo build --verbose 21 | - name: Run tests 22 | run: cargo test --verbose 23 | - name: Run Clippy 24 | run: cargo clippy --verbose -- -D warnings 25 | - name: Check formatting 26 | run: cargo fmt --check 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "srtlib" 3 | version = "0.2.0" 4 | authors = ["Konstantinos Gavalas "] 5 | edition = "2021" 6 | description = "A simple library for handling .srt subtitle files" 7 | readme = "README.md" 8 | repository = "https://github.com/gavalasdev/srtlib" 9 | license = "MIT OR Apache-2.0" 10 | keywords = ["subtitle", "subtitles", "srt", "parsing", "parser"] 11 | categories = ["filesystem", "parser-implementations"] 12 | exclude = [ 13 | ".github" 14 | ] 15 | 16 | [dependencies] 17 | encoding_rs = "0.8.24" 18 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Konstantinos Gavalas 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # srtlib 2 | 3 | [![Build Status](https://img.shields.io/github/actions/workflow/status/gavalasdev/srtlib/rust.yml?branch=master)](https://github.com/galenkod/srtlib/actions?query=workflow%3ARust) 4 | [![Version](https://img.shields.io/crates/v/srtlib)](https://crates.io/crates/srtlib) 5 | [![Crates.io Total Downloads](https://img.shields.io/crates/d/srtlib)](https://crates.io/crates/srtlib) 6 | ![License](https://img.shields.io/crates/l/srtlib) 7 | 8 | A simple Rust library for handling .srt subtitle files. 9 | 10 | * [`srtlib` documentation](https://docs.rs/srtlib) 11 | 12 | srtlib allows you to handle subtitle files as collections of multiple subtitle structs, letting you modify the subtitles without directly messing with the .srt files. 13 | 14 | Subtitle collections can be generated by parsing strings or files, but also from the ground up, enabling total control of all the elements of each subtitle. 15 | 16 | ## Usage 17 | 18 | Add this to your Cargo.toml: 19 | ```toml 20 | [dependencies] 21 | srtlib = "0.2" 22 | ``` 23 | To read a .srt file: 24 | ```rust 25 | use srtlib::Subtitles; 26 | 27 | // Parse subtitles from file that uses the utf-8 encoding. 28 | let mut subs = Subtitles::parse_from_file("subtitles.srt", None).unwrap(); 29 | ``` 30 | 31 | You can now perform any action you want on the subtitles. 32 | For example to move all subtitles 10 seconds forward in time: 33 | 34 | ```rust 35 | // Move every subtitle 10 seconds forward in time. 36 | for s in &mut subs { 37 | s.add_seconds(10); 38 | } 39 | ``` 40 | 41 | Finally we can write the subtitles back to a .srt file: 42 | 43 | ```rust 44 | subs.write_to_file("subtitles_fixed.srt", None).unwrap(); 45 | ``` 46 | For more examples refer to the [documentation](https://docs.rs/srtlib). 47 | 48 | ## License 49 | 50 | Licensed under either of 51 | 52 | * Apache License, Version 2.0 53 | ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) 54 | * MIT license 55 | ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) 56 | 57 | at your option. 58 | 59 | ## Contribution 60 | 61 | Unless you explicitly state otherwise, any contribution intentionally submitted 62 | for inclusion in the work by you, as defined in the Apache-2.0 license, shall be 63 | dual licensed as above, without any additional terms or conditions. 64 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Konstantinos Gavalas. 2 | // 3 | // Licensed under the Apache License, Version 2.0 or the MIT license 5 | // , at your 6 | // option. This file may not be copied, modified, or distributed 7 | // except according to those terms. 8 | 9 | //! A simple library for handling .srt subtitle files. 10 | //! 11 | //! This library allows you to handle subtitle files as collections of multiple subtitle structs, 12 | //! letting you modify the subtitles without directly messing with the .srt files. 13 | //! 14 | //! Subtitle collections can be generated by parsing strings and files, but also from the ground 15 | //! up, enabling total control of all the elements of each subtitle. 16 | //! 17 | //! # Examples 18 | //! ```no_run 19 | //! use srtlib::Subtitles; 20 | //! 21 | //! // Parse subtitles from file that uses the utf-8 encoding. 22 | //! let mut subs = Subtitles::parse_from_file("subtitles.srt", None).unwrap(); 23 | //! 24 | //! // Move every subtitle 10 seconds forward in time. 25 | //! for s in &mut subs { 26 | //! s.add_seconds(10); 27 | //! } 28 | //! 29 | //! // Write subtitles back to the same .srt file. 30 | //! subs.write_to_file("subtitles.srt", None).unwrap(); 31 | //! ``` 32 | //! 33 | //! ```no_run 34 | //! use srtlib::{Timestamp, Subtitle, Subtitles}; 35 | //! 36 | //! // Construct a new, empty Subtitles collection. 37 | //! let mut subs = Subtitles::new(); 38 | //! 39 | //! // Construct a new subtitle. 40 | //! let one = Subtitle::new(1, Timestamp::new(0, 0, 0, 0), Timestamp::new(0, 0, 2, 0), "Hello world!".to_string()); 41 | //! 42 | //! // Add subtitle at the end of the subs collection. 43 | //! subs.push(one); 44 | //! 45 | //! // Construct a new subtitle by parsing a string. 46 | //! let two = Subtitle::parse("2\n00:00:02,500 --> 00:00:05,000\nThis is a subtitle.".to_string()).unwrap(); 47 | //! 48 | //! // Add subtitle at the end of the subs collection. 49 | //! subs.push(two); 50 | //! 51 | //! // Write the subtitles to a .srt file. 52 | //! subs.write_to_file("test.srt", None).unwrap(); 53 | //! ``` 54 | //! 55 | //! ``` 56 | //! use std::fmt::Write; 57 | //! use srtlib::Subtitles; 58 | //! 59 | //! # fn main() -> Result<(), srtlib::ParsingError> { 60 | //! // Parse subtitles from a string and convert to vector. 61 | //! let mut subs = Subtitles::parse_from_str("3\n00:00:05,000 --> 00:00:07,200\nFoobar\n\n\ 62 | //! 1\n00:00:00,000 --> 00:00:02,400\nHello\n\n\ 63 | //! 2\n00:00:03,000 --> 00:00:05,000\nWorld\n\n".to_string() 64 | //! )?.to_vec(); 65 | //! 66 | //! // Sort the subtitles. 67 | //! subs.sort(); 68 | //! 69 | //! // Collect all subtitle text into a string. 70 | //! let mut res = String::new(); 71 | //! for s in subs { 72 | //! write!(&mut res, "{}\n", s.text).unwrap(); 73 | //! } 74 | //! 75 | //! assert_eq!(res, "Hello\nWorld\nFoobar\n".to_string()); 76 | //! 77 | //! # Ok(()) 78 | //! # } 79 | //! 80 | //! ``` 81 | 82 | use encoding_rs::*; 83 | use std::fmt; 84 | use std::fs; 85 | use std::io::prelude::*; 86 | use std::ops::Index; 87 | use std::path::Path; 88 | /// The number of milliseconds in a second. 89 | const ONE_SECOND_MILLIS: u32 = 1000; 90 | /// The number of milliseconds in a minute. 91 | const ONE_MINUTE_MILLIS: u32 = 60 * ONE_SECOND_MILLIS; 92 | /// The number of milliseconds in an hour. 93 | const ONE_HOUR_MILLIS: u32 = 60 * ONE_MINUTE_MILLIS; 94 | 95 | /// The error type returned by any function that parses strings or files. 96 | #[derive(Debug)] 97 | pub enum ParsingError { 98 | ParseIntError(std::num::ParseIntError), 99 | IOError(std::io::Error), 100 | MalformedTimestamp, 101 | BadSubtitleStructure(usize), 102 | BadEncodingName, 103 | } 104 | 105 | impl fmt::Display for ParsingError { 106 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 107 | match self { 108 | ParsingError::ParseIntError(error) => write!(f, "{}", error), 109 | ParsingError::IOError(error) => write!(f, "{}", error), 110 | ParsingError::MalformedTimestamp => write!(f, "tried parsing a malformed timestamp"), 111 | ParsingError::BadEncodingName => write!(f, "incorrect encoding name provided; refer to https://encoding.spec.whatwg.org/#names-and-labels for available encodings"), 112 | ParsingError::BadSubtitleStructure(num) => { 113 | let number = if num > &0 { num.to_string() } else { String::from("unknown") }; 114 | write!(f, "tried parsing an incorrectly formatted subtitle (subtitle number {})", number) 115 | } 116 | 117 | } 118 | } 119 | } 120 | 121 | impl std::error::Error for ParsingError {} 122 | 123 | impl From for ParsingError { 124 | fn from(error: std::num::ParseIntError) -> Self { 125 | ParsingError::ParseIntError(error) 126 | } 127 | } 128 | 129 | impl From for ParsingError { 130 | fn from(error: std::io::Error) -> Self { 131 | ParsingError::IOError(error) 132 | } 133 | } 134 | 135 | /// A simple timestamp following the timecode format hours:minutes:seconds,milliseconds. 136 | /// 137 | /// Used within the [`Subtitle`] struct to indicate the time that the subtitle should appear on 138 | /// screen(start_time) and the time it should disappear(end_time). 139 | /// The maximum value for any given Timestamp is 255:59:59,999. 140 | /// 141 | /// # Examples 142 | /// 143 | /// We can directly construct Timestamps from integers and they will always be displayed with the 144 | /// correct timecode format: 145 | /// ``` 146 | /// use srtlib::Timestamp; 147 | /// 148 | /// let time = Timestamp::new(0, 0, 1, 200); 149 | /// assert_eq!(time.to_string(), "00:00:01,200"); 150 | /// ``` 151 | /// 152 | /// We can also, for example, construct the Timestamp by parsing a string, move it forward in time by 65 seconds and then 153 | /// print it in the correct format. 154 | /// ``` 155 | /// use srtlib::Timestamp; 156 | /// 157 | /// let mut time = Timestamp::parse("00:01:10,314").unwrap(); 158 | /// time.add_seconds(65); 159 | /// assert_eq!(time.to_string(), "00:02:15,314"); 160 | /// ``` 161 | /// 162 | /// [`Subtitle`]: struct.Subtitle.html 163 | #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Hash)] 164 | pub struct Timestamp { 165 | milliseconds: u32, 166 | } 167 | 168 | impl Timestamp { 169 | /// The maximum value for any given Timestamp. 170 | pub const MAX_TIMESTAMP_MILLIS: u32 = 171 | 255 * ONE_HOUR_MILLIS + 59 * ONE_MINUTE_MILLIS + 59 * ONE_SECOND_MILLIS + 999; 172 | /// Converts hours, minutes, seconds and milliseconds to milliseconds. 173 | pub fn convert_to_milliseconds(hours: u8, minutes: u8, seconds: u8, milliseconds: u16) -> u32 { 174 | (((hours as u32 * 60) + minutes as u32) * 60 + seconds as u32) * 1000 + milliseconds as u32 175 | } 176 | /// Constructs a new Timestamp from integers. 177 | pub fn new(hours: u8, minutes: u8, seconds: u8, milliseconds: u16) -> Timestamp { 178 | Timestamp { 179 | milliseconds: Self::convert_to_milliseconds(hours, minutes, seconds, milliseconds), 180 | } 181 | } 182 | /// Constructs a new Timestamp from milliseconds. 183 | pub fn from_milliseconds(millis: u32) -> Timestamp { 184 | Timestamp { 185 | milliseconds: millis, 186 | } 187 | } 188 | 189 | /// Constructs a new Timestamp by parsing a string with the format 190 | /// "hours:minutes:seconds,milliseconds". 191 | /// 192 | /// # Errors 193 | /// If this function encounters a string that does not follow the correct timecode format, a 194 | /// MalformedTimestamp error variant will be returned. 195 | pub fn parse(s: &str) -> Result { 196 | let mut iter = s.splitn(3, ':'); 197 | let hours = iter 198 | .next() 199 | .ok_or(ParsingError::MalformedTimestamp)? 200 | .parse()?; 201 | let minutes = iter 202 | .next() 203 | .ok_or(ParsingError::MalformedTimestamp)? 204 | .parse()?; 205 | let mut second_iter = iter 206 | .next() 207 | .ok_or(ParsingError::MalformedTimestamp)? 208 | .splitn(2, &[',', '.']); 209 | let seconds = second_iter 210 | .next() 211 | .ok_or(ParsingError::MalformedTimestamp)? 212 | .parse()?; 213 | let milliseconds = second_iter 214 | .next() 215 | .ok_or(ParsingError::MalformedTimestamp)? 216 | .parse()?; 217 | 218 | Ok(Timestamp::new(hours, minutes, seconds, milliseconds)) 219 | } 220 | 221 | /// Moves the timestamp n hours forward in time. 222 | /// Negative values may be provided in order to move the timestamp back in time. 223 | /// 224 | /// # Panics 225 | /// 226 | /// Panics if we exceed the upper limit or go below zero. 227 | pub fn add_hours(&mut self, n: i64) { 228 | self.add_milliseconds(n * ONE_HOUR_MILLIS as i64); 229 | } 230 | 231 | /// Moves the timestamp n minutes forward in time. 232 | /// Negative values may be provided in order to move the timestamp back in time. 233 | /// 234 | /// # Panics 235 | /// 236 | /// Panics if we exceed the upper limit or go below zero. 237 | pub fn add_minutes(&mut self, n: i64) { 238 | self.add_milliseconds(n * ONE_MINUTE_MILLIS as i64); 239 | } 240 | 241 | /// Moves the timestamp n seconds forward in time. 242 | /// Negative values may be provided in order to move the timestamp back in time. 243 | /// 244 | /// # Panics 245 | /// 246 | /// Panics if we exceed the upper limit or go below zero. 247 | pub fn add_seconds(&mut self, n: i64) { 248 | self.add_milliseconds(n * ONE_SECOND_MILLIS as i64); 249 | } 250 | 251 | /// Moves the timestamp n milliseconds forward in time. 252 | /// Negative values may be provided in order to move the timestamp back in time. 253 | /// 254 | /// # Panics 255 | /// 256 | /// Panics if we exceed the upper limit or go below zero. 257 | pub fn add_milliseconds(&mut self, n: i64) { 258 | let millis: i64 = self.milliseconds as i64 + n; 259 | if millis < 0 || millis > Self::MAX_TIMESTAMP_MILLIS as i64 { 260 | panic!("Surpassed limits of Timestamp!"); 261 | } 262 | self.milliseconds = millis as u32; 263 | } 264 | 265 | /// Moves the timestamp forward in time by an amount specified as timestamp. 266 | /// 267 | /// # Panics 268 | /// 269 | /// Panics if we exceed the upper limit 270 | pub fn add(&mut self, timestamp: &Timestamp) { 271 | self.add_milliseconds(timestamp.milliseconds as i64); 272 | } 273 | 274 | /// Moves the timestamp backward in time by an amount specified as timestamp. 275 | /// 276 | /// # Panics 277 | /// 278 | /// Panics if we go below zero 279 | pub fn sub(&mut self, timestamp: &Timestamp) { 280 | self.add_milliseconds(-(timestamp.milliseconds as i64)); 281 | } 282 | 283 | /// Returns the timestamp as a tuple of four integers (hours, minutes, seconds, milliseconds). 284 | pub fn get(&self) -> (u8, u8, u8, u16) { 285 | let mut millis = self.milliseconds; 286 | let hours = (self.milliseconds / ONE_HOUR_MILLIS) as u8; 287 | millis -= (hours as u32) * ONE_HOUR_MILLIS; 288 | let minuts = (millis / ONE_MINUTE_MILLIS) as u8; 289 | millis -= (minuts as u32) * ONE_MINUTE_MILLIS; 290 | let seconds = (millis / ONE_SECOND_MILLIS) as u8; 291 | millis -= (seconds as u32) * ONE_SECOND_MILLIS; 292 | (hours, minuts, seconds, millis as u16) 293 | } 294 | 295 | /// Changes the timestamp according to the given integer values. 296 | pub fn set(&mut self, hours: u8, minutes: u8, seconds: u8, milliseconds: u16) { 297 | self.milliseconds = 298 | Timestamp::convert_to_milliseconds(hours, minutes, seconds, milliseconds); 299 | } 300 | } 301 | 302 | impl fmt::Display for Timestamp { 303 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 304 | let (hours, minutes, seconds, milliseconds) = self.get(); 305 | write!( 306 | f, 307 | "{:02}:{:02}:{:02},{:03}", 308 | hours, minutes, seconds, milliseconds 309 | ) 310 | } 311 | } 312 | 313 | /// A single subtitle. 314 | /// 315 | /// Contains the numeric counter, the beginning and end timestamps and the text of the subtitle. 316 | /// 317 | /// # Examples 318 | /// 319 | /// ``` 320 | /// use srtlib::Subtitle; 321 | /// use srtlib::Timestamp; 322 | /// 323 | /// let sub = Subtitle::new(1, Timestamp::new(0, 0, 0, 0), Timestamp::new(0, 0, 1, 0), "Hello world".to_string()); 324 | /// assert_eq!(sub.to_string(), "1\n00:00:00,000 --> 00:00:01,000\nHello world"); 325 | /// 326 | /// let sub = Subtitle::parse("2\n00:00:01,500 --> 00:00:02,500\nFooBar".to_string()).unwrap(); 327 | /// assert_eq!(sub.text, "FooBar"); 328 | /// ``` 329 | #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] 330 | pub struct Subtitle { 331 | pub num: usize, 332 | pub start_time: Timestamp, 333 | pub end_time: Timestamp, 334 | pub text: String, 335 | } 336 | 337 | impl Subtitle { 338 | /// Constructs a new Subtitle. 339 | pub fn new(num: usize, start_time: Timestamp, end_time: Timestamp, text: String) -> Subtitle { 340 | Subtitle { 341 | num, 342 | start_time, 343 | end_time, 344 | text, 345 | } 346 | } 347 | 348 | /// Construct a new subtitle by parsing a string with the format "num\nstart --> end\ntext" or the format 349 | /// "num\nstart --> end position_information\ntext" where start and end are timestamps using the format 350 | /// hours:minutes:seconds,milliseconds ; and position_information is position information of any format 351 | /// 352 | /// # Errors 353 | /// 354 | /// If this function encounters anything unexpected while parsing the string, a corresponding error variant 355 | /// will be returned. 356 | pub fn parse(input: String) -> Result { 357 | let mut iter = input.trim_start_matches('\n').splitn(3, '\n'); 358 | let num = iter 359 | .next() 360 | .ok_or(ParsingError::BadSubtitleStructure(0))? 361 | .parse::()?; 362 | let time = iter.next().ok_or(ParsingError::BadSubtitleStructure(num))?; 363 | let mut time_iter = time.split(" --> "); 364 | let start = Timestamp::parse( 365 | time_iter 366 | .next() 367 | .ok_or(ParsingError::BadSubtitleStructure(num))?, 368 | )?; 369 | let end_with_possible_position_info = time_iter 370 | .next() 371 | .ok_or(ParsingError::BadSubtitleStructure(num))?; 372 | let end = Timestamp::parse( 373 | end_with_possible_position_info 374 | .split(' ') 375 | .next() 376 | .ok_or(ParsingError::BadSubtitleStructure(num))?, 377 | )?; 378 | let text = iter.next().unwrap_or_default(); 379 | Ok(Subtitle::new(num, start, end, text.to_string())) 380 | } 381 | 382 | /// Moves the start and end timestamps n hours forward in time. 383 | /// Negative values may be provided in order to move the timestamps back in time. 384 | /// 385 | /// # Panics 386 | /// 387 | /// Panics if we exceed the upper limit or go below zero. 388 | pub fn add_hours(&mut self, n: i64) { 389 | self.start_time.add_hours(n); 390 | self.end_time.add_hours(n); 391 | } 392 | 393 | /// Moves the start and end timestamps n minutes forward in time. 394 | /// Negative values may be provided in order to move the timestamps back in time. 395 | /// 396 | /// # Panics 397 | /// 398 | /// Panics if we exceed the upper limit or go below zero. 399 | pub fn add_minutes(&mut self, n: i64) { 400 | self.start_time.add_minutes(n); 401 | self.end_time.add_minutes(n); 402 | } 403 | 404 | /// Moves the start and end timestamps n seconds forward in time. 405 | /// Negative values may be provided in order to move the timestamps back in time. 406 | /// 407 | /// # Panics 408 | /// 409 | /// Panics if we exceed the upper limit or go below zero. 410 | pub fn add_seconds(&mut self, n: i64) { 411 | self.start_time.add_seconds(n); 412 | self.end_time.add_seconds(n); 413 | } 414 | 415 | /// Moves the start and end timestamps n milliseconds forward in time. 416 | /// Negative values may be provided in order to move the timestamps back in time. 417 | /// 418 | /// # Panics 419 | /// 420 | /// Panics if we exceed the upper limit or go below zero. 421 | pub fn add_milliseconds(&mut self, n: i64) { 422 | self.start_time.add_milliseconds(n); 423 | self.end_time.add_milliseconds(n); 424 | } 425 | 426 | /// Moves the start and end timestamps forward in time by an amount specified as timestamp. 427 | /// 428 | /// # Panics 429 | /// 430 | /// Panics if we exceed the upper limit 431 | pub fn add(&mut self, timestamp: &Timestamp) { 432 | self.start_time.add(timestamp); 433 | self.end_time.add(timestamp); 434 | } 435 | 436 | /// Moves the start and end timestamps backward in time by an amount specified as timestamp. 437 | /// 438 | /// # Panics 439 | /// 440 | /// Panics if we go below zero 441 | pub fn sub(&mut self, timestamp: &Timestamp) { 442 | self.start_time.sub(timestamp); 443 | self.end_time.sub(timestamp); 444 | } 445 | } 446 | 447 | impl fmt::Display for Subtitle { 448 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 449 | write!( 450 | f, 451 | "{}\n{} --> {}\n{}", 452 | self.num, self.start_time, self.end_time, self.text 453 | ) 454 | } 455 | } 456 | 457 | /// A collection of [`Subtitle`] structs. 458 | /// 459 | /// Provides an easy way to represent an entire .srt subtitle file. 460 | /// 461 | /// # Examples 462 | /// 463 | /// ``` 464 | /// use srtlib::{Subtitle, Subtitles}; 465 | /// 466 | /// let mut subs = Subtitles::new(); 467 | /// subs.push(Subtitle::parse("1\n00:00:00,000 --> 00:00:01,000\nHello world!".to_string()).unwrap()); 468 | /// subs.push(Subtitle::parse("2\n00:00:01,200 --> 00:00:03,100\nThis is a subtitle!".to_string()).unwrap()); 469 | /// 470 | /// assert_eq!(subs.to_string(), 471 | /// "1\n00:00:00,000 --> 00:00:01,000\nHello world!\n\n2\n00:00:01,200 --> 00:00:03,100\nThis is a subtitle!"); 472 | /// ``` 473 | /// 474 | /// [`Subtitle`]: struct.Subtitle.html 475 | #[derive(Debug, Clone, Default, PartialEq, Eq)] 476 | pub struct Subtitles(Vec); 477 | 478 | impl Subtitles { 479 | /// Constructs a new(empty) Subtitles collection. 480 | pub fn new() -> Subtitles { 481 | Default::default() 482 | } 483 | 484 | /// Constructs a new Subtitles collection from a vector of [`Subtitle`] structs. 485 | /// 486 | /// [`Subtitle`]: struct.Subtitle.html 487 | pub fn new_from_vec(v: Vec) -> Subtitles { 488 | Subtitles(v) 489 | } 490 | 491 | /// Constructs a new Subtitles collection by parsing a string with the format 492 | /// "subtitle\n\nsubtitle\n\n..." where subtitle is a string formatted as described in the 493 | /// [`Subtitle`] struct documentation. 494 | /// 495 | /// # Errors 496 | /// 497 | /// If this function encounters anything unexpected while parsing the string, a corresponding error variant 498 | /// will be returned. 499 | /// 500 | /// [`Subtitle`]: struct.Subtitle.html 501 | pub fn parse_from_str(mut input: String) -> Result { 502 | let mut res = Subtitles::new(); 503 | 504 | input = input.trim_start_matches('\u{feff}').to_string(); 505 | if input.contains('\r') { 506 | input = input.replace('\r', ""); 507 | } 508 | 509 | for s in input 510 | .split_terminator("\n\n") 511 | // only parse lines that include alphanumeric characters 512 | .filter(|&x| x.contains(char::is_alphanumeric)) 513 | { 514 | res.push(Subtitle::parse(s.to_string())?); 515 | } 516 | 517 | Ok(res) 518 | } 519 | 520 | /// Constructs a new Subtitles collection by parsing a .srt file. 521 | /// 522 | /// **encoding** should either be Some("encoding-name") or None if using utf-8. 523 | /// For example if the file is using the ISO-8859-7 encoding (informally referred to as 524 | /// Latin/Greek) we could use: 525 | /// ```no_run 526 | /// use srtlib::Subtitles; 527 | /// # fn main() -> Result<(), srtlib::ParsingError> { 528 | /// let subs = Subtitles::parse_from_file("subtitles.srt", Some("iso-8859-7"))?; 529 | /// # Ok(()) 530 | /// # } 531 | /// ``` 532 | /// or the equivalent: 533 | /// ```no_run 534 | /// # use srtlib::Subtitles; 535 | /// # fn main() -> Result<(), srtlib::ParsingError> { 536 | /// let subs = Subtitles::parse_from_file("subtitles.srt", Some("greek"))?; 537 | /// # Ok(()) 538 | /// # } 539 | /// ``` 540 | /// For a list of encoding names (labels) refer to the [Encoding Standard]. 541 | /// 542 | /// # Errors 543 | /// 544 | /// If the encoding label provided is not one of the labels specified by the [Encoding 545 | /// Standard], a BadEncodingName error 546 | /// variant will be returned. 547 | /// 548 | /// If something unexpected is encountered during the parsing of the contents of the file, a 549 | /// corresponding error variant will be returned. 550 | /// 551 | /// [Encoding Standard]: https://encoding.spec.whatwg.org/#names-and-labels 552 | pub fn parse_from_file( 553 | path: impl AsRef, 554 | encoding: Option<&str>, 555 | ) -> Result { 556 | let mut f = fs::File::open(path)?; 557 | if let Some(enc) = encoding { 558 | let mut buffer = Vec::new(); 559 | f.read_to_end(&mut buffer)?; 560 | let (cow, ..) = Encoding::for_label(enc.as_bytes()) 561 | .ok_or(ParsingError::BadEncodingName)? 562 | .decode(buffer.as_slice()); 563 | Subtitles::parse_from_str(cow[..].to_string()) 564 | } else { 565 | let mut buffer = String::new(); 566 | f.read_to_string(&mut buffer)?; 567 | Subtitles::parse_from_str(buffer) 568 | } 569 | } 570 | 571 | /// Writes the contents of this Subtitles collection to a .srt file with the correct formatting. 572 | /// 573 | /// **encoding** should either be Some("encoding-name") or None if using utf-8. 574 | /// For example if the file is using the ISO-8859-7 encoding (informally referred to as 575 | /// Latin/Greek) we could use: 576 | /// ```no_run 577 | /// use srtlib::Subtitles; 578 | /// 579 | /// let subs = Subtitles::new(); 580 | /// // Work with the subtitles... 581 | /// subs.write_to_file("output.srt", Some("iso-8859-7")).unwrap(); 582 | /// ``` 583 | /// or the equivalent: 584 | /// ```no_run 585 | /// # use srtlib::Subtitles; 586 | /// # let subs = Subtitles::new(); 587 | /// subs.write_to_file("output.srt", Some("greek")).unwrap(); 588 | /// ``` 589 | /// For a list of encoding names (labels) refer to the [Encoding Standard]. 590 | /// 591 | /// # Errors 592 | /// 593 | /// If something goes wrong during the creation of the file using the specified path, an 594 | /// IOError error variant will be returned. 595 | /// 596 | /// If the encoding label provided is not one of the labels specified by the [Encoding 597 | /// Standard], a BadEncodingName error 598 | /// variant will be returned. 599 | /// 600 | /// [Encoding Standard]: https://encoding.spec.whatwg.org/#names-and-labels 601 | pub fn write_to_file( 602 | &self, 603 | path: impl AsRef, 604 | encoding: Option<&str>, 605 | ) -> Result<(), ParsingError> { 606 | let mut f = fs::File::create(path)?; 607 | if let Some(enc) = encoding { 608 | let string = &self.to_string(); 609 | let (cow, ..) = Encoding::for_label(enc.as_bytes()) 610 | .ok_or(ParsingError::BadEncodingName)? 611 | .encode(string); 612 | f.write_all(&cow)?; 613 | } else { 614 | f.write_all(self.to_string().as_bytes())?; 615 | } 616 | 617 | Ok(()) 618 | } 619 | 620 | /// Returns the Subtitles collection as a simple vector of [`Subtitle`] structs. 621 | /// 622 | /// [`Subtitle`]: struct.Subtitle.html 623 | pub fn to_vec(self) -> Vec { 624 | self.0 625 | } 626 | 627 | /// Returns the number of Subtitles in the collection. 628 | pub fn len(&self) -> usize { 629 | self.0.len() 630 | } 631 | 632 | /// Checks if there are no subtitles in the collection. 633 | pub fn is_empty(&self) -> bool { 634 | self.0.is_empty() 635 | } 636 | 637 | /// Adds a new subtitle at the end of the subtitles. 638 | pub fn push(&mut self, sub: Subtitle) { 639 | self.0.push(sub); 640 | } 641 | 642 | /// Sorts the subtitles in place based on their numeric counter 643 | pub fn sort(&mut self) { 644 | self.0.sort(); 645 | } 646 | } 647 | 648 | impl IntoIterator for Subtitles { 649 | type Item = Subtitle; 650 | type IntoIter = std::vec::IntoIter; 651 | 652 | fn into_iter(self) -> Self::IntoIter { 653 | self.0.into_iter() 654 | } 655 | } 656 | 657 | impl<'l> IntoIterator for &'l Subtitles { 658 | type Item = &'l Subtitle; 659 | type IntoIter = std::slice::Iter<'l, Subtitle>; 660 | 661 | fn into_iter(self) -> Self::IntoIter { 662 | self.0.iter() 663 | } 664 | } 665 | 666 | impl<'l> IntoIterator for &'l mut Subtitles { 667 | type Item = &'l mut Subtitle; 668 | type IntoIter = std::slice::IterMut<'l, Subtitle>; 669 | 670 | fn into_iter(self) -> Self::IntoIter { 671 | self.0.iter_mut() 672 | } 673 | } 674 | 675 | impl> Index for Subtitles { 676 | type Output = I::Output; 677 | 678 | fn index(&self, i: I) -> &Self::Output { 679 | &self.0[i] 680 | } 681 | } 682 | 683 | impl fmt::Display for Subtitles { 684 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 685 | if !self.is_empty() { 686 | let mut s = String::new(); 687 | for sub in &self[..self.len() - 1] { 688 | s.push_str(&format!("{}\n\n", &sub.to_string())); 689 | } 690 | s.push_str(&self[self.len() - 1].to_string()); 691 | write!(f, "{}", s) 692 | } else { 693 | Ok(()) 694 | } 695 | } 696 | } 697 | 698 | mod tests { 699 | #![allow(unused_imports)] 700 | use super::*; 701 | 702 | #[test] 703 | fn add_time_timestamp() { 704 | let mut timestamp = Timestamp::new(0, 0, 0, 0); 705 | timestamp.add_milliseconds(1200); 706 | assert_eq!(timestamp, Timestamp::new(0, 0, 1, 200)); 707 | timestamp.add_seconds(65); 708 | assert_eq!(timestamp, Timestamp::new(0, 1, 6, 200)); 709 | timestamp.add_minutes(122); 710 | assert_eq!(timestamp, Timestamp::new(2, 3, 6, 200)); 711 | timestamp.add_hours(-1); 712 | assert_eq!(timestamp, Timestamp::new(1, 3, 6, 200)); 713 | timestamp.add_seconds(-7); 714 | assert_eq!(timestamp, Timestamp::new(1, 2, 59, 200)); 715 | } 716 | 717 | #[test] 718 | #[should_panic(expected = "Surpassed limits of Timestamp!")] 719 | fn timestamp_overflow_panic() { 720 | let mut timestamp = Timestamp::new(0, 0, 0, 0); 721 | timestamp.add_hours(255); 722 | timestamp.add_minutes(60); 723 | println!("Expected a panic, got: {}", timestamp); 724 | } 725 | #[test] 726 | #[should_panic(expected = "Surpassed limits of Timestamp!")] 727 | fn timestamp_negative_panic() { 728 | let mut timestamp = Timestamp::new(0, 0, 0, 0); 729 | timestamp.add_minutes(-10); 730 | println!("Expected a panic, got: {}", timestamp); 731 | } 732 | 733 | #[test] 734 | fn timestamp_parsing() { 735 | assert_eq!( 736 | Timestamp::parse("12:35:42,756").unwrap(), 737 | Timestamp::new(12, 35, 42, 756) 738 | ); 739 | assert_eq!( 740 | Timestamp::parse("32:00:46,000").unwrap(), 741 | Timestamp::new(32, 0, 46, 000) 742 | ); 743 | assert_eq!( 744 | Timestamp::parse("12:35:42.756").unwrap(), 745 | Timestamp::new(12, 35, 42, 756) 746 | ); 747 | assert_eq!( 748 | Timestamp::parse("32:00:46.000").unwrap(), 749 | Timestamp::new(32, 0, 46, 000) 750 | ); 751 | } 752 | 753 | #[test] 754 | fn timestamp_to_str() { 755 | assert_eq!(Timestamp::new(0, 0, 0, 0).to_string(), "00:00:00,000"); 756 | assert_eq!(Timestamp::new(0, 1, 20, 500).to_string(), "00:01:20,500"); 757 | } 758 | 759 | #[test] 760 | fn subtitle_parsing() { 761 | let input = "1\n00:00:00,000 --> 00:00:01,000\nHello world!\nNew line!"; 762 | let result = Subtitle::new( 763 | 1, 764 | Timestamp::new(0, 0, 0, 0), 765 | Timestamp::new(0, 0, 1, 0), 766 | "Hello world!\nNew line!".to_string(), 767 | ); 768 | 769 | assert_eq!(Subtitle::parse(input.to_string()).unwrap(), result); 770 | } 771 | 772 | #[test] 773 | fn subtitle_ordering() { 774 | let sub1 = 775 | Subtitle::parse("1\n00:00:00,000 --> 00:00:02,000\nHello world!".to_string()).unwrap(); 776 | let sub2 = Subtitle::parse("2\n00:00:02,500 --> 00:00:05,000\nTest subtitle.".to_string()) 777 | .unwrap(); 778 | let sub3 = 779 | Subtitle::parse("2\n00:00:03,500 --> 00:00:06,000\nTest subtitle two.".to_string()) 780 | .unwrap(); 781 | 782 | assert!(sub1 < sub2); 783 | assert!(sub2 < sub3); 784 | } 785 | 786 | #[test] 787 | fn add_time_subtitle() { 788 | let mut sub = 789 | Subtitle::parse("1\n00:00:00,000 --> 00:00:02,000\nHello world!".to_string()).unwrap(); 790 | sub.add_seconds(10); 791 | assert_eq!( 792 | sub.to_string(), 793 | "1\n00:00:10,000 --> 00:00:12,000\nHello world!" 794 | ); 795 | sub.add_seconds(110); 796 | assert_eq!( 797 | sub.to_string(), 798 | "1\n00:02:00,000 --> 00:02:02,000\nHello world!" 799 | ); 800 | let t1 = Timestamp::new(0, 0, 0, 0); 801 | let t2 = Timestamp::new(1, 20, 0, 0); 802 | sub.add(&t1); 803 | assert_eq!( 804 | sub.to_string(), 805 | "1\n00:02:00,000 --> 00:02:02,000\nHello world!" 806 | ); 807 | sub.add(&t2); 808 | assert_eq!( 809 | sub.to_string(), 810 | "1\n01:22:00,000 --> 01:22:02,000\nHello world!" 811 | ); 812 | } 813 | 814 | #[test] 815 | fn sub_time_subtitle() { 816 | let mut sub = 817 | Subtitle::parse("1\n01:22:10,000 --> 01:22:12,000\nHello world!".to_string()).unwrap(); 818 | sub.add_seconds(-120); 819 | assert_eq!( 820 | sub.to_string(), 821 | "1\n01:20:10,000 --> 01:20:12,000\nHello world!" 822 | ); 823 | sub.add_seconds(-10); 824 | assert_eq!( 825 | sub.to_string(), 826 | "1\n01:20:00,000 --> 01:20:02,000\nHello world!" 827 | ); 828 | let t1 = Timestamp::new(0, 0, 0, 0); 829 | let t2 = Timestamp::new(1, 20, 0, 0); 830 | sub.sub(&t1); 831 | assert_eq!( 832 | sub.to_string(), 833 | "1\n01:20:00,000 --> 01:20:02,000\nHello world!" 834 | ); 835 | sub.sub(&t2); 836 | assert_eq!( 837 | sub.to_string(), 838 | "1\n00:00:00,000 --> 00:00:02,000\nHello world!" 839 | ); 840 | } 841 | 842 | #[test] 843 | fn sub_to_string() { 844 | let input = Subtitle::new( 845 | 1, 846 | Timestamp::new(0, 0, 0, 0), 847 | Timestamp::new(0, 0, 1, 0), 848 | "Hello world!\nNew line!".to_string(), 849 | ); 850 | let result = "1\n00:00:00,000 --> 00:00:01,000\nHello world!\nNew line!"; 851 | 852 | assert_eq!(input.to_string(), result); 853 | } 854 | 855 | #[test] 856 | fn subtitles_from_str_parsing() { 857 | let subs = "1\n00:00:00,000 --> 00:00:01,000\nHello world!\nExtra!\n\n\ 858 | 2\n00:00:01,500 --> 00:00:02,500\nThis is a subtitle!"; 859 | 860 | let parsed_subs = Subtitles::parse_from_str(subs.to_string()).unwrap(); 861 | assert_eq!( 862 | parsed_subs[0], 863 | Subtitle::new( 864 | 1, 865 | Timestamp::new(0, 0, 0, 0), 866 | Timestamp::new(0, 0, 1, 0), 867 | "Hello world!\nExtra!".to_string() 868 | ) 869 | ); 870 | assert_eq!( 871 | parsed_subs[1], 872 | Subtitle::new( 873 | 2, 874 | Timestamp::new(0, 0, 1, 500), 875 | Timestamp::new(0, 0, 2, 500), 876 | "This is a subtitle!".to_string() 877 | ) 878 | ); 879 | } 880 | 881 | #[test] 882 | fn sort_subtitles() { 883 | let subs = "2\n00:00:01,500 --> 00:00:02,500\nThis is a subtitle!\n\n\ 884 | 1\n00:00:00,000 --> 00:00:01,000\nHello world!\nExtra!\n\n\ 885 | 3\n00:00:02,500 --> 00:00:03,000\nFinal subtitle.\n"; 886 | 887 | let mut parsed_subs = Subtitles::parse_from_str(subs.to_string()).unwrap(); 888 | parsed_subs.sort(); 889 | 890 | let true_sort = "1\n00:00:00,000 --> 00:00:01,000\nHello world!\nExtra!\n\n\ 891 | 2\n00:00:01,500 --> 00:00:02,500\nThis is a subtitle!\n\n\ 892 | 3\n00:00:02,500 --> 00:00:03,000\nFinal subtitle.\n"; 893 | let sorted_subs = Subtitles::parse_from_str(true_sort.to_string()).unwrap(); 894 | 895 | assert_eq!(parsed_subs, sorted_subs); 896 | } 897 | 898 | #[test] 899 | fn empty_subtitles_display() { 900 | let out = Subtitles::new().to_string(); 901 | assert_eq!(out, String::new()); 902 | } 903 | 904 | #[test] 905 | fn empty_subtitles_parse() { 906 | let subs = Subtitles::parse_from_str(String::new()).expect("Failed to parse empty subs"); 907 | assert_eq!(subs.len(), 0); 908 | } 909 | 910 | #[test] 911 | fn subtitle_with_position_information() { 912 | let input = "1\n00:00:07,001 --> 00:00:09,015 position:50,00%,middle align:middle size:80,00% line:84,67%\nThis is a subtitle text"; 913 | let result = Subtitle::new( 914 | 1, 915 | Timestamp::new(0, 0, 7, 1), 916 | Timestamp::new(0, 0, 9, 15), 917 | "This is a subtitle text".to_string(), 918 | ); 919 | 920 | assert_eq!(Subtitle::parse(input.to_string()).unwrap(), result); 921 | } 922 | 923 | #[test] 924 | fn empty_text_for_timestamp() { 925 | let subs = "1\n00:00:00,000 --> 00:00:01,000\n\n\n\ 926 | 2\n00:00:01,500 --> 00:00:02,500\nThis is a subtitle!"; 927 | 928 | let parsed_subs = Subtitles::parse_from_str(subs.to_string()).unwrap(); 929 | assert_eq!( 930 | parsed_subs[0], 931 | Subtitle::new( 932 | 1, 933 | Timestamp::new(0, 0, 0, 0), 934 | Timestamp::new(0, 0, 1, 0), 935 | String::new(), 936 | ) 937 | ); 938 | assert_eq!( 939 | parsed_subs[1], 940 | Subtitle::new( 941 | 2, 942 | Timestamp::new(0, 0, 1, 500), 943 | Timestamp::new(0, 0, 2, 500), 944 | "This is a subtitle!".to_string() 945 | ) 946 | ); 947 | } 948 | } 949 | --------------------------------------------------------------------------------