├── .travis.yml ├── tests ├── mixed.m3u ├── ext.m3u ├── read.rs └── write.rs ├── .gitignore ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── src ├── lib.rs ├── write.rs └── read.rs └── README.md /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | rust: 3 | - nightly 4 | - beta 5 | - stable 6 | script: 7 | - cargo build --verbose 8 | - cargo test --verbose 9 | - cargo doc --verbose 10 | -------------------------------------------------------------------------------- /tests/mixed.m3u: -------------------------------------------------------------------------------- 1 | # Example taken from the M3U wikipedia entry 2 | 3 | Alternative\Band - Song.mp3 4 | Classical\Other Band - New Song.mp3 5 | Stuff.mp3 6 | D:\More Music\Foo.mp3 7 | ..\Other Music\Bar.mp3 8 | http://emp.cx:8000/Listen.pls 9 | http://www.example.com/~user/Mine.mp3 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *~ 3 | *# 4 | *.o 5 | *.so 6 | *.swp 7 | *.dylib 8 | *.dSYM 9 | *.dll 10 | *.rlib 11 | *.dummy 12 | *.exe 13 | *-test 14 | /bin/main 15 | /bin/test-internal 16 | /bin/test-external 17 | /doc/ 18 | /target/ 19 | /build/ 20 | /.rust/ 21 | rusti.sh 22 | /examples/**/target 23 | 24 | Cargo.lock 25 | -------------------------------------------------------------------------------- /tests/ext.m3u: -------------------------------------------------------------------------------- 1 | #EXTM3U 2 | 3 | #EXTINF:123, Sample artist - Sample title 4 | C:\Documents and Settings\I\My Music\Sample.mp3 5 | 6 | #EXTINF:321,Example Artist - Example title 7 | C:\Documents and Settings\I\My Music\Greatest Hits\Example.ogg 8 | 9 | # Comment about this listing 10 | #EXTINF:123, Sample artist - Sample title 11 | Sample.mp3 12 | 13 | #EXTINF:321,Example Artist - Example title 14 | Greatest Hits\Example.ogg 15 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "m3u" 3 | description = "A crate for reading and writing `.m3u` files - the de facto standard for multimedia playlists." 4 | version = "1.0.0" 5 | authors = ["mitchmindtree "] 6 | readme = "README.md" 7 | keywords = ["m3u", "playlist", "multimedia", "music", "audio"] 8 | license = "MIT OR Apache-2.0" 9 | repository = "https://github.com/mitchmindtree/m3u.git" 10 | homepage = "https://github.com/mitchmindtree/m3u" 11 | 12 | [dependencies] 13 | url = "1.2.4" 14 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Copyright 2016 RustAudio Developers 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 RustAudio Developers 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 | -------------------------------------------------------------------------------- /tests/read.rs: -------------------------------------------------------------------------------- 1 | extern crate m3u; 2 | 3 | #[test] 4 | fn mixed() { 5 | let path = std::path::Path::new("tests/mixed.m3u"); 6 | let mut reader = m3u::Reader::open(path).unwrap(); 7 | let mut entries = reader.entries(); 8 | 9 | assert_eq!(entries.next().unwrap().unwrap(), 10 | m3u::path_entry(r"Alternative\Band - Song.mp3")); 11 | assert_eq!(entries.next().unwrap().unwrap(), 12 | m3u::path_entry(r"Classical\Other Band - New Song.mp3")); 13 | assert_eq!(entries.next().unwrap().unwrap(), 14 | m3u::path_entry(r"Stuff.mp3")); 15 | assert_eq!(entries.next().unwrap().unwrap(), 16 | m3u::path_entry(r"D:\More Music\Foo.mp3")); 17 | assert_eq!(entries.next().unwrap().unwrap(), 18 | m3u::path_entry(r"..\Other Music\Bar.mp3")); 19 | assert_eq!(entries.next().unwrap().unwrap(), 20 | m3u::url_entry(r"http://emp.cx:8000/Listen.pls").unwrap()); 21 | assert_eq!(entries.next().unwrap().unwrap(), 22 | m3u::url_entry(r"http://www.example.com/~user/Mine.mp3").unwrap()); 23 | assert!(entries.next().is_none()); 24 | } 25 | 26 | #[test] 27 | fn ext() { 28 | let expected = vec![ 29 | m3u::path_entry(r"C:\Documents and Settings\I\My Music\Sample.mp3") 30 | .extend(123.0, "Sample artist - Sample title"), 31 | m3u::path_entry(r"C:\Documents and Settings\I\My Music\Greatest Hits\Example.ogg") 32 | .extend(321.0, "Example Artist - Example title"), 33 | m3u::path_entry(r"Sample.mp3") 34 | .extend(123.0, "Sample artist - Sample title"), 35 | m3u::path_entry(r"Greatest Hits\Example.ogg") 36 | .extend(321.0, "Example Artist - Example title"), 37 | ]; 38 | 39 | let path = std::path::Path::new("tests/ext.m3u"); 40 | let mut reader = m3u::Reader::open_ext(path).unwrap(); 41 | let entries: Vec<_> = reader.entry_exts().map(|e| e.unwrap()).collect(); 42 | 43 | assert_eq!(&entries, &expected); 44 | } 45 | -------------------------------------------------------------------------------- /tests/write.rs: -------------------------------------------------------------------------------- 1 | extern crate m3u; 2 | 3 | #[test] 4 | fn entry() { 5 | 6 | // Create a multimedia playlist. 7 | let playlist = vec![ 8 | m3u::path_entry(r"Alternative\Band - Song.mp3"), 9 | m3u::path_entry(r"Classical\Other Band - New Song.mp3"), 10 | m3u::path_entry(r"Stuff.mp3"), 11 | m3u::path_entry(r"D:\More Music\Foo.mp3"), 12 | m3u::path_entry(r"..\Other Music\Bar.mp3"), 13 | m3u::url_entry(r"http://emp.cx:8000/Listen.pls").unwrap(), 14 | m3u::url_entry(r"http://www.example.com/~user/Mine.mp3").unwrap(), 15 | ]; 16 | 17 | const FILEPATH: &'static str = "tests/playlist.m3u"; 18 | 19 | if std::path::Path::new(FILEPATH).exists() { 20 | std::fs::remove_file(FILEPATH).unwrap(); 21 | } 22 | 23 | // Write the playlist to the file. 24 | { 25 | let mut file = std::fs::File::create(FILEPATH).unwrap(); 26 | let mut writer = m3u::Writer::new(&mut file); 27 | for entry in &playlist { 28 | writer.write_entry(entry).unwrap(); 29 | } 30 | writer.flush().unwrap(); 31 | } 32 | 33 | // Read the playlist from the file. 34 | { 35 | let mut reader = m3u::Reader::open(FILEPATH).unwrap(); 36 | let read_playlist: Vec<_> = reader.entries().map(|entry| entry.unwrap()).collect(); 37 | assert_eq!(&playlist, &read_playlist); 38 | } 39 | 40 | std::fs::remove_file(FILEPATH).unwrap(); 41 | } 42 | 43 | #[test] 44 | fn entry_ext() { 45 | 46 | // Create a multimedia playlist, including the duration in seconds and name for each entry. 47 | let playlist = vec![ 48 | m3u::path_entry(r"C:\Documents and Settings\I\My Music\Sample.mp3") 49 | .extend(123.0, "Sample artist - Sample title"), 50 | m3u::path_entry(r"C:\Documents and Settings\I\My Music\Greatest Hits\Example.ogg") 51 | .extend(321.0, "Example Artist - Example title"), 52 | m3u::path_entry(r"Sample.mp3") 53 | .extend(123.0, "Sample artist - Sample title"), 54 | m3u::path_entry(r"Greatest Hits\Example.ogg") 55 | .extend(321.0, "Example Artist - Example title"), 56 | ]; 57 | 58 | const FILEPATH: &'static str = "tests/playlist_ext.m3u"; 59 | 60 | if std::path::Path::new(FILEPATH).exists() { 61 | std::fs::remove_file(FILEPATH).unwrap(); 62 | } 63 | 64 | // Write the playlist to the file. 65 | { 66 | let mut file = std::fs::File::create(FILEPATH).unwrap(); 67 | let mut writer = m3u::Writer::new_ext(&mut file).unwrap(); 68 | for entry in &playlist { 69 | writer.write_entry(entry).unwrap(); 70 | } 71 | writer.flush().unwrap(); 72 | } 73 | 74 | // Read the playlist from the file. 75 | { 76 | let mut reader = m3u::Reader::open_ext(FILEPATH).unwrap(); 77 | let read_playlist: Vec<_> = reader.entry_exts().map(|entry| entry.unwrap()).collect(); 78 | assert_eq!(&playlist, &read_playlist); 79 | } 80 | 81 | std::fs::remove_file(FILEPATH).unwrap(); 82 | } 83 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! A crate for reading and writing the **M3U** format. 2 | //! 3 | //! The **M3U** format is considered the de facto standard for multimedia playlists. 4 | //! 5 | //! There is no formal specification for the **M3U** format. This crate is implemented based on the 6 | //! rough description under the format's current wikipedia entry. 7 | 8 | #![warn(missing_docs)] 9 | 10 | pub extern crate url; 11 | 12 | mod read; 13 | mod write; 14 | 15 | pub use read::{Reader, EntryReader, EntryExtReader, Entries, EntryExts, 16 | EntryExtReaderConstructionError, ReadEntryExtError}; 17 | pub use write::{Writer, EntryWriter, EntryExtWriter}; 18 | pub use url::Url; 19 | 20 | /// An entry in an **M3U** multimedia playlist. 21 | /// 22 | /// Describes the source of the media. 23 | /// 24 | /// In rare cases an `Entry` may point to another `.m3u` file. If a user wishes to support this in 25 | /// their application, they must be sure to handle cycles within the **M3U** graph. 26 | #[derive(Clone, Debug, Hash, PartialEq)] 27 | pub enum Entry { 28 | /// The entry resides at the given `Path`. 29 | /// 30 | /// The `Path` may be either absolute or relative. 31 | /// 32 | /// Note that the `Path` may also point to a directory. After starting, the media player would 33 | /// play all contents of the directory. 34 | Path(std::path::PathBuf), 35 | /// The entry can be found at the given `Url`. 36 | Url(url::Url), 37 | } 38 | 39 | /// An entry with some associated extra information. 40 | #[derive(Clone, Debug, PartialEq)] 41 | pub struct EntryExt { 42 | /// The M3U entry. Can be either a `Path` or `Url`. 43 | pub entry: Entry, 44 | /// Extra information associated with the M3U entry. 45 | pub extinf: ExtInf, 46 | } 47 | 48 | /// Extra information associated with an M3U entry. 49 | #[derive(Clone, Debug, PartialEq)] 50 | pub struct ExtInf { 51 | /// The duration of the media's runtime in seconds. 52 | /// 53 | /// Note that some `m3u` extended formats specify streams with a `-1` duration. 54 | pub duration_secs: f64, 55 | /// The name of the media. E.g. "Aphex Twin - Windowlicker". 56 | pub name: String, 57 | } 58 | 59 | 60 | impl Entry { 61 | 62 | /// Whether or not the `Entry` is a `Path`. 63 | pub fn is_path(&self) -> bool { 64 | match *self { 65 | Entry::Path(_) => true, 66 | Entry::Url(_) => false, 67 | } 68 | } 69 | 70 | /// Whether or not the `Entry` is a `Url`. 71 | pub fn is_url(&self) -> bool { 72 | match *self { 73 | Entry::Url(_) => true, 74 | Entry::Path(_) => false, 75 | } 76 | } 77 | 78 | /// Extend the entry with extra information including the duration in seconds and a name. 79 | pub fn extend(self, duration_secs: f64, name: N) -> EntryExt 80 | where N: Into, 81 | { 82 | EntryExt { 83 | extinf: ExtInf { duration_secs: duration_secs, name: name.into() }, 84 | entry: self, 85 | } 86 | } 87 | 88 | } 89 | 90 | /// A helper function to simplify creation of the `Entry`'s `Path` variant. 91 | pub fn path_entry

(path: P) -> Entry 92 | where P: Into, 93 | { 94 | Entry::Path(path.into()) 95 | } 96 | 97 | /// A helper function to simplify creation of the `Entry`'s `Url` variant. 98 | pub fn url_entry(url: &str) -> Result { 99 | Url::parse(url).map(|url| Entry::Url(url)) 100 | } 101 | -------------------------------------------------------------------------------- /src/write.rs: -------------------------------------------------------------------------------- 1 | use {Entry, EntryExt}; 2 | use std; 3 | use std::io::Write; 4 | 5 | /// A writer that accepts entries of type `E` and writes the associated M3U format. 6 | /// 7 | /// Entries are always written using in UTF-8. 8 | pub struct Writer 9 | where W: Write, 10 | { 11 | /// The writer to which the `M3U` format is written. 12 | writer: W, 13 | /// Used for buffering lines as bytes for writing. 14 | line_buffer: Vec, 15 | /// The type of entries that will be written. 16 | entry: std::marker::PhantomData, 17 | } 18 | 19 | /// A `Writer` that specifically writes `Entry`s. 20 | pub type EntryWriter = Writer; 21 | /// A `Writer` that specifically writes `EntryExt`s. 22 | pub type EntryExtWriter = Writer; 23 | 24 | impl Writer 25 | where W: Write, 26 | { 27 | 28 | fn new_inner(writer: W, line_buffer: Vec) -> Self { 29 | Writer { 30 | writer: writer, 31 | line_buffer: line_buffer, 32 | entry: std::marker::PhantomData, 33 | } 34 | } 35 | 36 | /// `Flush` the `writer` output stream, ensuring that all intermediately buffered entries reach 37 | /// their destination. 38 | /// 39 | /// This should be called after all entries have been written. 40 | /// 41 | /// If it is not called, the destructor will finalize the file, but any errors that occur in 42 | /// the process cannot be handled. 43 | pub fn flush(mut self) -> Result<(), std::io::Error> { 44 | self.writer.flush() 45 | } 46 | 47 | } 48 | 49 | impl EntryWriter 50 | where W: Write, 51 | { 52 | 53 | /// Create a writer that writes the original, non_extended M3U `Entry` type. 54 | pub fn new(writer: W) -> Self { 55 | Self::new_inner(writer, Vec::new()) 56 | } 57 | 58 | /// Attempt to write the given `Entry` to the given `writer`. 59 | /// 60 | /// Writes the `Path` or `Url` in plain text, ending with a newline. 61 | pub fn write_entry(&mut self, entry: &Entry) -> Result<(), std::io::Error> { 62 | let Writer { ref mut writer, ref mut line_buffer, .. } = *self; 63 | line_buffer.clear(); 64 | try!(write_entry(line_buffer, entry)); 65 | writer.write_all(line_buffer) 66 | } 67 | 68 | } 69 | 70 | impl EntryExtWriter 71 | where W: Write, 72 | { 73 | 74 | /// Create a writer that writes extended M3U `EntryExt`s. 75 | /// 76 | /// The `#EXTM3U` header line is written immediately. 77 | pub fn new_ext(mut writer: W) -> Result { 78 | let mut line_buffer = Vec::new(); 79 | try!(writeln!(&mut line_buffer, "#EXTM3U")); 80 | try!(writer.write_all(&line_buffer)); 81 | Ok(Self::new_inner(writer, line_buffer)) 82 | } 83 | 84 | /// Attempt to write the given `EntryExt` to the given `writer`. 85 | /// 86 | /// First writes the `#EXTINF:` line, then writes the entry line. 87 | pub fn write_entry(&mut self, entry_ext: &EntryExt) -> Result<(), std::io::Error> { 88 | let Writer { ref mut writer, ref mut line_buffer, .. } = *self; 89 | line_buffer.clear(); 90 | let extinf = &entry_ext.extinf; 91 | try!(writeln!(line_buffer, "#EXTINF:{},{}", extinf.duration_secs, &extinf.name)); 92 | try!(write_entry(line_buffer, &entry_ext.entry)); 93 | writer.write_all(line_buffer) 94 | } 95 | 96 | } 97 | 98 | /// Write the given `Entry` into the given `line_buffer`. 99 | /// 100 | /// Writes the `Path` or `Url` in plain text, ending with a newline. 101 | fn write_entry(line_buffer: &mut Vec, entry: &Entry) -> Result<(), std::io::Error> { 102 | match *entry { 103 | Entry::Path(ref path) => writeln!(line_buffer, "{}", path.display()), 104 | Entry::Url(ref url) => writeln!(line_buffer, "{}", url), 105 | } 106 | } 107 | 108 | 109 | impl Drop for Writer 110 | where W: Write, 111 | { 112 | fn drop(&mut self) { 113 | self.writer.flush().ok(); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | m3u [![Build Status](https://travis-ci.org/mitchmindtree/m3u.svg?branch=master)](https://travis-ci.org/mitchmindtree/m3u) [![Crates.io](https://img.shields.io/crates/v/m3u.svg)](https://crates.io/crates/m3u) [![Crates.io](https://img.shields.io/crates/l/m3u.svg)](https://github.com/mitchmindtree/m3u/blob/master/LICENSE-MIT) [![docs.rs](https://docs.rs/m3u/badge.svg)](https://docs.rs/m3u/) 2 | === 3 | 4 | A lib for reading and writing `.m3u` files - the de facto standard for multimedia playlists. 5 | 6 | Example 7 | ------- 8 | 9 | ### Original M3U 10 | 11 | ```rust 12 | extern crate m3u; 13 | 14 | fn main() { 15 | 16 | // Create a multimedia media playlist. 17 | let playlist = vec![ 18 | m3u::path_entry(r"Alternative\Band - Song.mp3"), 19 | m3u::path_entry(r"Classical\Other Band - New Song.mp3"), 20 | m3u::path_entry(r"Stuff.mp3"), 21 | m3u::path_entry(r"D:\More Music\Foo.mp3"), 22 | m3u::path_entry(r"..\Other Music\Bar.mp3"), 23 | m3u::url_entry(r"http://emp.cx:8000/Listen.pls").unwrap(), 24 | m3u::url_entry(r"http://www.example.com/~user/Mine.mp3").unwrap(), 25 | ]; 26 | 27 | // Write the playlist to a file. 28 | { 29 | let mut file = std::fs::File::create("playlist.m3u").unwrap(); 30 | let mut writer = m3u::Writer::new(&mut file); 31 | for entry in &playlist { 32 | writer.write_entry(entry).unwrap(); 33 | } 34 | } 35 | 36 | // Read the playlist from the file. 37 | let mut reader = m3u::Reader::open("playlist.m3u").unwrap(); 38 | let read_playlist: Vec<_> = reader.entries().map(|entry| entry.unwrap()).collect(); 39 | assert_eq!(&playlist, &read_playlist); 40 | } 41 | ``` 42 | 43 | Writes then reads a plain text UTF-8 file that looks like this: 44 | 45 | ```m3u 46 | Alternative\Band - Song.mp3 47 | Classical\Other Band - New Song.mp3 48 | Stuff.mp3 49 | D:\More Music\Foo.mp3 50 | ..\Other Music\Bar.mp3 51 | http://emp.cx:8000/Listen.pls 52 | http://www.example.com/~user/Mine.mp3 53 | ``` 54 | 55 | ### Extended M3U 56 | 57 | ```rust 58 | extern crate m3u; 59 | 60 | fn main() { 61 | 62 | // Create a multimedia playlist, including the duration in seconds and name for each entry. 63 | let playlist = vec![ 64 | m3u::path_entry(r"C:\Documents and Settings\I\My Music\Sample.mp3") 65 | .extend(123.0, "Sample artist - Sample title"), 66 | m3u::path_entry(r"C:\Documents and Settings\I\My Music\Greatest Hits\Example.ogg") 67 | .extend(321.0, "Example Artist - Example title"), 68 | m3u::path_entry(r"Sample.mp3") 69 | .extend(123.0, "Sample artist - Sample title"), 70 | m3u::path_entry(r"Greatest Hits\Example.ogg") 71 | .extend(321.0, "Example Artist - Example title"), 72 | ]; 73 | 74 | // Write the playlist to the file. 75 | { 76 | let mut file = std::fs::File::create("playlist_ext.m3u").unwrap(); 77 | let mut writer = m3u::Writer::new_ext(&mut file).unwrap(); 78 | for entry in &playlist { 79 | writer.write_entry(entry).unwrap(); 80 | } 81 | } 82 | 83 | // Read the playlist from the file. 84 | let mut reader = m3u::Reader::open_ext("playlist_ext.m3u").unwrap(); 85 | let read_playlist: Vec<_> = reader.entry_exts().map(|entry| entry.unwrap()).collect(); 86 | assert_eq!(&playlist, &read_playlist); 87 | } 88 | ``` 89 | 90 | Writes then reads a plain text UTF-8 file in the Extended M3U format that looks like this: 91 | 92 | ```m3u 93 | #EXTM3U 94 | #EXTINF:123,Sample artist - Sample title 95 | C:\Documents and Settings\I\My Music\Sample.mp3 96 | #EXTINF:321,Example Artist - Example title 97 | C:\Documents and Settings\I\My Music\Greatest Hits\Example.ogg 98 | #EXTINF:123,Sample artist - Sample title 99 | Sample.mp3 100 | #EXTINF:321,Example Artist - Example title 101 | Greatest Hits\Example.ogg 102 | ``` 103 | 104 | License 105 | ------- 106 | 107 | Licensed under either of 108 | 109 | * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) 110 | * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) 111 | 112 | at your option. 113 | 114 | 115 | **Contributions** 116 | 117 | Unless you explicitly state otherwise, any contribution intentionally submitted 118 | for inclusion in the work by you, as defined in the Apache-2.0 license, shall be 119 | dual licensed as above, without any additional terms or conditions. 120 | -------------------------------------------------------------------------------- /src/read.rs: -------------------------------------------------------------------------------- 1 | use {Entry, EntryExt, ExtInf}; 2 | use std; 3 | use url; 4 | 5 | /// A reader that reads the `M3U` format from the underlying reader. 6 | /// 7 | /// A `Reader` is a streaming reader. It reads data from the underlying reader on demand and reads 8 | /// no more than strictly necessary. 9 | /// 10 | /// The inner `reader` `R` must be some buffered reader as the "#EXTM3U" header, "#EXTINF:" tags 11 | /// and entries are each read from a single line of plain text. 12 | /// 13 | /// A `Reader` will only attempt to read entries of type `E`. 14 | pub struct Reader 15 | where R: std::io::BufRead, 16 | { 17 | /// The reader from which the `M3U` format is read. 18 | reader: R, 19 | /// String used for buffering read lines. 20 | line_buffer: String, 21 | /// The entry type that the `reader` will read. 22 | entry: std::marker::PhantomData, 23 | } 24 | 25 | /// A `Reader` that specifically reads `Entry`s. 26 | pub type EntryReader = Reader; 27 | /// A `Reader` that specifically reads `EntryExt`s. 28 | pub type EntryExtReader = Reader; 29 | 30 | /// An iterator that yields `Entry`s. 31 | /// 32 | /// All `Entry`s are lazily read from the inner buffered reader. 33 | pub struct Entries<'r, R> 34 | where R: 'r + std::io::BufRead, 35 | { 36 | reader: &'r mut EntryReader, 37 | } 38 | 39 | /// An iterator that yields `EntryExt`s. 40 | /// 41 | /// All `EntryExt`s are lazily read from the inner buffered reader. 42 | pub struct EntryExts<'r, R> 43 | where R: 'r + std::io::BufRead, 44 | { 45 | reader: &'r mut EntryExtReader, 46 | } 47 | 48 | /// Errors that may occur when constructing a new `Reader`. 49 | #[derive(Debug)] 50 | pub enum EntryExtReaderConstructionError { 51 | /// The "#EXTM3U" header was not found in the first line when attempting to 52 | /// construct a `Reader` from some given `Reader`. 53 | HeaderNotFound, 54 | /// Errors produced by the `BufRead::read_line` method. 55 | BufRead(std::io::Error), 56 | } 57 | 58 | /// Errors that may occur when attempting to read an `EntryExt` from a read line `str`. 59 | #[derive(Debug)] 60 | pub enum ReadEntryExtError { 61 | /// Either the "#EXTINF:" tag was not found for the `EntryExt` or the duration and name 62 | /// following the tag were not correctly formatted. 63 | /// 64 | /// Assuming that the tag was simply omitted, the line will instead be parsed as an `Entry`. 65 | ExtInfNotFound(Entry), 66 | /// Errors produced by the `BufRead::read_line` method. 67 | BufRead(std::io::Error), 68 | } 69 | 70 | 71 | impl Reader 72 | where R: std::io::BufRead, 73 | { 74 | 75 | fn new_inner(reader: R, line_buffer: String) -> Self { 76 | Reader { 77 | reader: reader, 78 | line_buffer: line_buffer, 79 | entry: std::marker::PhantomData, 80 | } 81 | } 82 | 83 | /// Produce the inner `reader`. 84 | pub fn into_inner(self) -> R { 85 | self.reader 86 | } 87 | 88 | } 89 | 90 | impl EntryReader 91 | where R: std::io::BufRead, 92 | { 93 | 94 | /// Create a reader that reads the original, non-extended M3U `Entry` type. 95 | pub fn new(reader: R) -> Self { 96 | Self::new_inner(reader, String::new()) 97 | } 98 | 99 | /// Attempt to read the next `Entry` from the inner reader. 100 | /// 101 | /// Returns `Ok(None)` when there are no more lines. 102 | /// 103 | /// Returns an `Err(std::io::Error)` if an error occurs when calling the inner `reader`'s 104 | /// `BufRead::read_line` method. 105 | fn read_next_entry(&mut self) -> Result, std::io::Error> { 106 | let Reader { ref mut reader, ref mut line_buffer, .. } = *self; 107 | read_next_entry(reader, line_buffer) 108 | } 109 | 110 | /// Produce an iterator that yields `Entry`s. 111 | /// 112 | /// All `Entry`s are lazily read from the inner buffered reader. 113 | pub fn entries(&mut self) -> Entries { 114 | Entries { reader: self } 115 | } 116 | 117 | } 118 | 119 | impl EntryExtReader 120 | where R: std::io::BufRead, 121 | { 122 | 123 | /// Create a reader that reads the extended M3U `EntryExt` type. 124 | /// 125 | /// The `#EXTM3U` header is read immediately. 126 | /// 127 | /// Reading `EntryExt`s will be done on demand. 128 | pub fn new_ext(mut reader: R) -> Result { 129 | let mut line_buffer = String::new(); 130 | 131 | loop { 132 | let num_read_bytes = try!(reader.read_line(&mut line_buffer)); 133 | let line = line_buffer.trim_left(); 134 | 135 | // The first line of the extended M3U format should always be the "#EXTM3U" header. 136 | const HEADER: &'static str = "#EXTM3U"; 137 | if line.len() >= HEADER.len() && &line[..HEADER.len()] == HEADER { 138 | break; 139 | } 140 | 141 | // Skip any empty lines that might be present at the top of the file. 142 | if num_read_bytes != 0 && line.is_empty() { 143 | continue; 144 | } 145 | 146 | // If the first non-empty line was not the header, return an error. 147 | return Err(EntryExtReaderConstructionError::HeaderNotFound); 148 | } 149 | 150 | Ok(Self::new_inner(reader, line_buffer)) 151 | } 152 | 153 | /// Attempt to read the next `EntryExt` from the inner reader. 154 | /// 155 | /// This method attempts to read two non-empty, non-comment lines. 156 | /// 157 | /// The first is checked for the `EXTINF` tag which is used to create an `ExtInf`. Upon failure 158 | /// an `ExtInfNotFound` error is returned and the line is instead parsed as an `Entry`. 159 | /// 160 | /// If an `#EXTINF:` tag was read, next line is parsed as an `Entry`. 161 | /// 162 | /// Returns `Ok(None)` when there are no more lines. 163 | fn read_next_entry(&mut self) -> Result, ReadEntryExtError> { 164 | let Reader { ref mut reader, ref mut line_buffer, .. } = *self; 165 | 166 | const TAG: &'static str = "#EXTINF:"; 167 | 168 | // Read an `ExtInf` from the given line. 169 | // 170 | // This function assumes the the line begins with "#EXTINF:" and will panic otherwise. 171 | fn read_extinf(mut line: &str) -> Option { 172 | line = &line[TAG.len()..]; 173 | 174 | // The duration and track title should be delimited by the first comma. 175 | let mut parts = line.splitn(2, ','); 176 | 177 | // Get the duration, or return `None` if there isn't any. 178 | let duration_secs = match parts.next().and_then(|s| s.parse().ok()) { 179 | Some(secs) => secs, 180 | None => return None, 181 | }; 182 | 183 | // Get the name or set it as an empty string. 184 | let name = parts.next().map(|s| s.trim().into()).unwrap_or_else(String::new); 185 | 186 | Some(ExtInf { 187 | duration_secs: duration_secs, 188 | name: name, 189 | }) 190 | } 191 | 192 | // Skip empty lines and comments until we find the "#EXTINF:" tag. 193 | loop { 194 | // Read the next line or return `None` if we're done. 195 | line_buffer.clear(); 196 | if try!(reader.read_line(line_buffer)) == 0 { 197 | return Ok(None); 198 | } 199 | 200 | let extinf = { 201 | let line = line_buffer.trim_left(); 202 | 203 | match line.chars().next() { 204 | // Skip empty lines. 205 | None => continue, 206 | // Distinguish between comments and the "#EXTINF:" tag. 207 | Some('#') => match line.len() >= TAG.len() && &line[..TAG.len()] == TAG { 208 | // Skip comments. 209 | false => continue, 210 | // We've found the "#EXTINF:" tag. 211 | true => read_extinf(line), 212 | }, 213 | // Assume the "#EXTINF:" tag was omitted and this was intended to be an `Entry`. 214 | // Due to the lack of official specification, it is unclear whether a mixture 215 | // of tagged and non-tagged entries should be supported for the EXTM3U format. 216 | Some(_) => { 217 | let entry = read_entry(line.trim_right()); 218 | return Err(ReadEntryExtError::ExtInfNotFound(entry)); 219 | }, 220 | } 221 | }; 222 | 223 | // Read the next non-empty, non-comment line as an entry. 224 | let entry = match try!(read_next_entry(reader, line_buffer)) { 225 | None => return Ok(None), 226 | Some(entry) => entry, 227 | }; 228 | 229 | return match extinf { 230 | Some(extinf) => Ok(Some(EntryExt { 231 | entry: entry, 232 | extinf: extinf, 233 | })), 234 | None => Err(ReadEntryExtError::ExtInfNotFound(entry)), 235 | } 236 | } 237 | } 238 | 239 | /// Produce an iterator that yields `EntryExt`s. 240 | /// 241 | /// All `EntryExt`s are lazily read from the inner buffered reader. 242 | pub fn entry_exts(&mut self) -> EntryExts { 243 | EntryExts { reader: self } 244 | } 245 | 246 | } 247 | 248 | impl EntryReader> { 249 | 250 | /// Attempts to create a reader that reads `Entry`s from the specified file. 251 | /// 252 | /// This is a convenience constructor that opens a `File`, wraps it in a `BufReader` and then 253 | /// constructs a `Reader` from it. 254 | pub fn open

(filename: P) -> Result 255 | where P: AsRef, 256 | { 257 | let file = try!(std::fs::File::open(filename)); 258 | let buf_reader = std::io::BufReader::new(file); 259 | Ok(Self::new(buf_reader)) 260 | } 261 | 262 | } 263 | 264 | impl EntryExtReader> { 265 | 266 | /// Attempts to create a reader that reads `EntryExt`s from the specified file. 267 | /// 268 | /// This is a convenience constructor that opens a `File`, wraps it in a `BufReader` and then 269 | /// constructs a `Reader` from it. 270 | pub fn open_ext

(filename: P) -> Result 271 | where P: AsRef, 272 | { 273 | let file = try!(std::fs::File::open(filename)); 274 | let buf_reader = std::io::BufReader::new(file); 275 | Self::new_ext(buf_reader) 276 | } 277 | 278 | } 279 | 280 | 281 | /// Attempt to read the next `Entry` from the inner reader. 282 | fn read_next_entry(reader: &mut R, line_buffer: &mut String) -> Result, std::io::Error> 283 | where R: std::io::BufRead, 284 | { 285 | loop { 286 | // Read the next line or return `None` if we're done. 287 | line_buffer.clear(); 288 | if try!(reader.read_line(line_buffer)) == 0 { 289 | return Ok(None); 290 | } 291 | 292 | let line = line_buffer.trim_left(); 293 | match line.chars().next() { 294 | // Skip empty lines. 295 | None => continue, 296 | // Skip comments. 297 | Some('#') => continue, 298 | // Break when we have a non-empty, non-comment line. 299 | _ => return Ok(Some(read_entry(line.trim_right()))), 300 | } 301 | } 302 | } 303 | 304 | /// Read an `Entry` from the given line. 305 | /// 306 | /// First attempts to read a URL entry. A URL is only returned if `Some` `host_str` is parsed. 307 | /// 308 | /// If a URL cannot be parsed, we assume the entry is a `Path`. 309 | fn read_entry(line: &str) -> Entry { 310 | if let Ok(url) = url::Url::parse(line) { 311 | if url.host_str().is_some() { 312 | return Entry::Url(url); 313 | } 314 | 315 | } 316 | Entry::Path(line.into()) 317 | } 318 | 319 | 320 | impl<'r, R> Iterator for Entries<'r, R> 321 | where R: std::io::BufRead, 322 | { 323 | type Item = Result; 324 | fn next(&mut self) -> Option { 325 | match self.reader.read_next_entry() { 326 | Ok(Some(entry)) => Some(Ok(entry)), 327 | Ok(None) => None, 328 | Err(err) => Some(Err(err)), 329 | } 330 | } 331 | } 332 | 333 | impl<'r, R> Iterator for EntryExts<'r, R> 334 | where R: std::io::BufRead, 335 | { 336 | type Item = Result; 337 | fn next(&mut self) -> Option { 338 | match self.reader.read_next_entry() { 339 | Ok(Some(entry)) => Some(Ok(entry)), 340 | Ok(None) => None, 341 | Err(err) => Some(Err(err)), 342 | } 343 | } 344 | } 345 | 346 | 347 | impl From for EntryExtReaderConstructionError { 348 | fn from(err: std::io::Error) -> Self { 349 | EntryExtReaderConstructionError::BufRead(err) 350 | } 351 | } 352 | 353 | impl From for ReadEntryExtError { 354 | fn from(err: std::io::Error) -> Self { 355 | ReadEntryExtError::BufRead(err) 356 | } 357 | } 358 | 359 | 360 | impl std::error::Error for EntryExtReaderConstructionError { 361 | fn description(&self) -> &str { 362 | match *self { 363 | EntryExtReaderConstructionError::HeaderNotFound => 364 | "the \"#EXTM3U\" header was not found", 365 | EntryExtReaderConstructionError::BufRead(ref err) => 366 | std::error::Error::description(err), 367 | } 368 | } 369 | fn cause(&self) -> Option<&std::error::Error> { 370 | match *self { 371 | EntryExtReaderConstructionError::HeaderNotFound => None, 372 | EntryExtReaderConstructionError::BufRead(ref err) => Some(err), 373 | } 374 | } 375 | } 376 | 377 | impl std::error::Error for ReadEntryExtError { 378 | fn description(&self) -> &str { 379 | match *self { 380 | ReadEntryExtError::ExtInfNotFound(_) => 381 | "the \"#EXTINF:\" tag was not found or was incorrectly formatted", 382 | ReadEntryExtError::BufRead(ref err) => 383 | std::error::Error::description(err), 384 | } 385 | } 386 | fn cause(&self) -> Option<&std::error::Error> { 387 | match *self { 388 | ReadEntryExtError::ExtInfNotFound(_) => None, 389 | ReadEntryExtError::BufRead(ref err) => Some(err), 390 | } 391 | } 392 | } 393 | 394 | 395 | impl std::fmt::Display for EntryExtReaderConstructionError { 396 | fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { 397 | match *self { 398 | EntryExtReaderConstructionError::HeaderNotFound => 399 | write!(f, "{}", std::error::Error::description(self)), 400 | EntryExtReaderConstructionError::BufRead(ref err) => 401 | err.fmt(f), 402 | } 403 | } 404 | } 405 | 406 | impl std::fmt::Display for ReadEntryExtError { 407 | fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { 408 | match *self { 409 | ReadEntryExtError::ExtInfNotFound(_) => 410 | write!(f, "{}", std::error::Error::description(self)), 411 | ReadEntryExtError::BufRead(ref err) => 412 | err.fmt(f), 413 | } 414 | } 415 | } 416 | --------------------------------------------------------------------------------