mirror of
https://github.com/euzu/tuliprox.git
synced 2026-09-14 13:12:13 +02:00
initial checkin
This commit is contained in:
+13
@@ -0,0 +1,13 @@
|
||||
/target
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
|
||||
*.m3u
|
||||
|
||||
.idea
|
||||
Generated
+260
@@ -0,0 +1,260 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "0.7.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ansi_term"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b"
|
||||
dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "atty"
|
||||
version = "0.2.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
|
||||
dependencies = [
|
||||
"hermit-abi",
|
||||
"libc",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "2.33.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002"
|
||||
dependencies = [
|
||||
"ansi_term",
|
||||
"atty",
|
||||
"bitflags",
|
||||
"strsim",
|
||||
"textwrap",
|
||||
"unicode-width",
|
||||
"vec_map",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dtoa"
|
||||
version = "0.4.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56899898ce76aaf4a0f24d914c97ea6ed976d42fec6ad33fcbb0a1103e07b2b0"
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.11.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.1.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "1.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.102"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2a5ac8f984bfcf3a823267e5fde638acc3325f6496633a5da6bb6eb2171e103"
|
||||
|
||||
[[package]]
|
||||
name = "linked-hash-map"
|
||||
version = "0.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3"
|
||||
|
||||
[[package]]
|
||||
name = "m3u-filter"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_yaml",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9f5105d4fdaab20335ca9565e106a5d9b82b6219b5ba735731124ac6711d23d"
|
||||
dependencies = [
|
||||
"unicode-xid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.6.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.130"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.130"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d7bc1a1ab1961464eae040d96713baa5a724a8152c1222492465b54322ec508b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_yaml"
|
||||
version = "0.8.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d8c608a35705a5d3cdc9fbe403147647ff34b921f8e833e49306df898f9b20af"
|
||||
dependencies = [
|
||||
"dtoa",
|
||||
"indexmap",
|
||||
"serde",
|
||||
"yaml-rust",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.76"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c6f107db402c2c2055242dbf4d2af0e69197202e9faacbef9571bbe47f5a1b84"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-xid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "textwrap"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060"
|
||||
dependencies = [
|
||||
"unicode-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-xid"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
|
||||
|
||||
[[package]]
|
||||
name = "vec_map"
|
||||
version = "0.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
|
||||
dependencies = [
|
||||
"winapi-i686-pc-windows-gnu",
|
||||
"winapi-x86_64-pc-windows-gnu",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-i686-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||
|
||||
[[package]]
|
||||
name = "winapi-x86_64-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "yaml-rust"
|
||||
version = "0.4.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85"
|
||||
dependencies = [
|
||||
"linked-hash-map",
|
||||
]
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
[package]
|
||||
name = "m3u-filter"
|
||||
version = "0.1.0"
|
||||
edition = "2018"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_yaml = "0.8"
|
||||
regex = "1"
|
||||
clap = "2"
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 euzu
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,62 @@
|
||||
#m3u-filter
|
||||
|
||||
m3u-filter is a simple application which can filter entries out of a playlist in EXTM3U format.
|
||||
If you have a playlist which contains unwanted entries, you can create filter which Include or Discard entries
|
||||
based on the header information of the playlist entries.
|
||||
Currently filter and rename operations support group, name and title fields.
|
||||
|
||||
You can define multiple targets for filtering if you want to create multiple playlists from a big playlist.
|
||||
|
||||
The config.yml file contains the filter and rename definitions. It should be located next to the exe file or in the current working directory.
|
||||
You can override this behaviour with the -c argument.
|
||||
The input file can be defined inside the config.yml file or can be given as -i argument.
|
||||
If given as argument, it overrides the config file entry.
|
||||
|
||||
the config.yml looks like:
|
||||
```yaml
|
||||
***REMOVED***
|
||||
filename: playlist.m3u
|
||||
***REMOVED***
|
||||
***REMOVED***
|
||||
***REMOVED***
|
||||
***REMOVED***
|
||||
***REMOVED***
|
||||
***REMOVED***
|
||||
***REMOVED***
|
||||
***REMOVED***
|
||||
***REMOVED***
|
||||
***REMOVED***
|
||||
***REMOVED***
|
||||
***REMOVED***
|
||||
***REMOVED***
|
||||
```
|
||||
|
||||
The filter is either Include mode or Discard mode.
|
||||
The regular expression syntax is similar to Perl-style regular expressions,
|
||||
but lacks a few features like look around and backreferences.
|
||||
|
||||
The rename supports capture groups. Each group can be adressed with $1, $2 .. in the new_name attribute.
|
||||
This is needed for players which do not observe the order and sort themselves. In the above example each entry starting
|
||||
with DE will be prefixed with "1.".
|
||||
If you dont care about sorting, you dont need the rename block.
|
||||
|
||||
|
||||
##The EXTM3U format is an extension of the M3U format.
|
||||
m3u has become almost a standard for the formation of playlists of media players and media devices.
|
||||
|
||||
A file in the EXTM3U format is a text file with the extension m3u or m3u8.
|
||||
|
||||
An example of the contents of the file in the EXTM3U format
|
||||
```
|
||||
#EXTM3U
|
||||
#PLAYLIST:Playlist1
|
||||
#EXTINF:-1 tvg-name="Channel 1" tvg-logo="http://site.domain / channel1_logo.png", Channel 1
|
||||
#EXTGRP:Group1
|
||||
http://site.domain/channel1
|
||||
#EXTINF:-1 tvg-name="Channel 2" tvg-logo="http://site.domain / channel2_logo.png", Channel 2
|
||||
#EXTGRP:Group1
|
||||
http://site.domain/channel2
|
||||
#EXTINF:-1 tvg-name="Channel 3" tvg-logo="http://site.domain / channel3_logo.png", Channel 3
|
||||
#EXTGRP:Group2
|
||||
http://site.domain/channel3 -- reports and targets
|
||||
```
|
||||
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="WEB_MODULE" version="4">
|
||||
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||
<exclude-output />
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/target" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
use regex::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub enum ItemField {
|
||||
Group,
|
||||
Name,
|
||||
Title,
|
||||
}
|
||||
|
||||
impl fmt::Display for ItemField {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match *self {
|
||||
ItemField::Group => write!(f, "Group"),
|
||||
ItemField::Name => write!(f, "Name"),
|
||||
ItemField::Title => write!(f, "Title"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub enum FilterMode {
|
||||
Discard,
|
||||
Include,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct ConfigFilter {
|
||||
pub field: ItemField,
|
||||
pub pattern: String,
|
||||
#[serde(skip_serializing, skip_deserializing)]
|
||||
pub re: Option<Regex>,
|
||||
}
|
||||
|
||||
impl ConfigFilter {
|
||||
pub fn prepare(&mut self) -> () {
|
||||
let re = regex::Regex::new(&self.pattern);
|
||||
if re.is_err() {
|
||||
println!("cant parse regex: {}", &self.pattern);
|
||||
std::process::exit(1);
|
||||
}
|
||||
self.re = Some(re.unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct ConfigFilters {
|
||||
pub mode: FilterMode,
|
||||
pub rules: Vec<ConfigFilter>,
|
||||
}
|
||||
|
||||
impl ConfigFilters {
|
||||
pub fn is_include(&self) -> bool {
|
||||
matches!(self.mode, FilterMode::Include)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct ConfigRename {
|
||||
pub field: ItemField,
|
||||
pub pattern: String,
|
||||
pub new_name: String,
|
||||
#[serde(skip_serializing, skip_deserializing)]
|
||||
pub re: Option<Regex>,
|
||||
}
|
||||
|
||||
impl ConfigRename {
|
||||
pub fn prepare(&mut self) -> () {
|
||||
let re = regex::Regex::new(&self.pattern);
|
||||
if re.is_err() {
|
||||
println!("cant parse regex: {}", &self.pattern);
|
||||
std::process::exit(1);
|
||||
}
|
||||
self.re = Some(re.unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct ConfigTarget {
|
||||
pub filename: String,
|
||||
pub filter: ConfigFilters,
|
||||
pub rename: Vec<ConfigRename>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct ConfigInput {
|
||||
pub filename: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Config {
|
||||
pub ***REMOVED*** ConfigInput,
|
||||
pub ***REMOVED*** Vec<ConfigTarget>,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn prepare(&mut self) {
|
||||
for t in &mut self.targets {
|
||||
for f in &mut t.filter.rules {
|
||||
f.prepare();
|
||||
}
|
||||
for r in &mut t.rename {
|
||||
r.prepare();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+259
@@ -0,0 +1,259 @@
|
||||
// https://de.wikipedia.org/wiki/M3U
|
||||
// https://siptv.eu/howto/playlist.html
|
||||
|
||||
use std::collections::hash_map::Entry;
|
||||
use std::collections::HashMap;
|
||||
use std::fmt;
|
||||
use std::str::Chars;
|
||||
|
||||
pub struct PlaylistItemHeader {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub logo: String,
|
||||
pub logo_small: String,
|
||||
pub group: String,
|
||||
pub title: String,
|
||||
pub parent_code: String,
|
||||
pub audio_track: String,
|
||||
pub time_shift: String,
|
||||
pub rec: String,
|
||||
pub source: String,
|
||||
}
|
||||
|
||||
impl Clone for PlaylistItemHeader {
|
||||
fn clone(&self) -> PlaylistItemHeader {
|
||||
PlaylistItemHeader {
|
||||
id: String::from(&self.id),
|
||||
name: String::from(&self.name),
|
||||
logo: String::from(&self.logo),
|
||||
logo_small: String::from(&self.logo_small),
|
||||
group: String::from(&self.group),
|
||||
title: String::from(&self.title),
|
||||
parent_code: String::from(&self.parent_code),
|
||||
audio_track: String::from(&self.audio_track),
|
||||
time_shift: String::from(&self.time_shift),
|
||||
rec: String::from(&self.rec),
|
||||
source: String::from(&self.source),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for PlaylistItemHeader {
|
||||
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
|
||||
fmt.write_str("id:")?;
|
||||
fmt.write_str(&self.id)?;
|
||||
fmt.write_str(", name:")?;
|
||||
fmt.write_str(&self.name)?;
|
||||
fmt.write_str(", logo:")?;
|
||||
fmt.write_str(&self.logo)?;
|
||||
fmt.write_str(", logo-small:")?;
|
||||
fmt.write_str(&self.logo_small)?;
|
||||
fmt.write_str(", group:")?;
|
||||
fmt.write_str(&self.group)?;
|
||||
fmt.write_str(", title:")?;
|
||||
fmt.write_str(&self.title)?;
|
||||
fmt.write_str(", parent-code:")?;
|
||||
fmt.write_str(&self.parent_code)?;
|
||||
fmt.write_str(", audio-track:")?;
|
||||
fmt.write_str(&self.audio_track)?;
|
||||
fmt.write_str(", timeshift:")?;
|
||||
fmt.write_str(&self.time_shift)?;
|
||||
fmt.write_str(", rec:")?;
|
||||
fmt.write_str(&self.rec)?;
|
||||
fmt.write_str(", source:")?;
|
||||
fmt.write_str(&self.source)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PlaylistItem {
|
||||
pub header: PlaylistItemHeader,
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
impl Clone for PlaylistItem {
|
||||
fn clone(&self) -> PlaylistItem {
|
||||
PlaylistItem {
|
||||
header: self.header.clone(),
|
||||
url: String::from(&self.url),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for PlaylistItem {
|
||||
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
|
||||
fmt.write_str("header:")?;
|
||||
fmt.write_str(&self.header.to_string())?;
|
||||
fmt.write_str(", url:")?;
|
||||
fmt.write_str(&self.url)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl PlaylistItem {
|
||||
pub fn to_m3u(&self) -> String {
|
||||
let mut line = format!("#EXTINF:-1 tvg-id=\"{}\" tvg-name=\"{}\" tvg-logo=\"{}\" group-title=\"{}\"", self.header.id, self.header.name, self.header.logo, self.header.group);
|
||||
if !self.header.logo_small.is_empty() {
|
||||
line = format!("{} tvg-logo-small=\"!{}\"", line, self.header.logo_small);
|
||||
}
|
||||
if !self.header.parent_code.is_empty() {
|
||||
line = format!("{} parent-code=\"!{}\"", line, self.header.parent_code);
|
||||
}
|
||||
if !self.header.audio_track.is_empty() {
|
||||
line = format!("{} audio-track=\"!{}\"", line, self.header.audio_track);
|
||||
}
|
||||
if !self.header.time_shift.is_empty() {
|
||||
line = format!("{} timeschift=\"!{}\"", line, self.header.time_shift);
|
||||
}
|
||||
if !self.header.rec.is_empty() {
|
||||
line = format!("{} rec=\"!{}\"", line, self.header.rec);
|
||||
}
|
||||
line = format!("{},{}\n{}", line, self.header.title, self.url);
|
||||
line
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PlaylistGroup {
|
||||
pub title: String,
|
||||
pub channels: Vec<PlaylistItem>,
|
||||
}
|
||||
|
||||
impl fmt::Display for PlaylistGroup {
|
||||
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
|
||||
fmt.write_str("title:")?;
|
||||
fmt.write_str(&self.title.to_string())?;
|
||||
fmt.write_str(", channels:")?;
|
||||
fmt.write_str(&self.channels.len().to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn token_value<'a>(it: &'a mut Chars) -> String {
|
||||
let oc = it.next();
|
||||
if oc.is_some() && oc.unwrap() == '"' {
|
||||
return get_value(it);
|
||||
}
|
||||
return String::from("");
|
||||
}
|
||||
|
||||
fn get_value<'a>(it: &'a mut Chars) -> String {
|
||||
let mut result: Vec<char> = vec![];
|
||||
let mut oc = it.next();
|
||||
while oc.is_some() && oc.unwrap() != '"' {
|
||||
result.push(oc.unwrap());
|
||||
oc = it.next();
|
||||
}
|
||||
return String::from(result.iter().collect::<String>());
|
||||
}
|
||||
|
||||
fn token_till(it: &mut Chars, stop_char: char) -> Option<String> {
|
||||
let mut result: Vec<char> = vec![];
|
||||
loop {
|
||||
let c = it.next();
|
||||
if !c.is_some() || c.unwrap() == stop_char {
|
||||
break;
|
||||
}
|
||||
if result.len() == 0 && c.unwrap().is_whitespace() {
|
||||
continue;
|
||||
}
|
||||
result.push(c.unwrap())
|
||||
}
|
||||
if result.len() > 0 { Some(String::from(result.iter().collect::<String>())) } else { None }
|
||||
}
|
||||
|
||||
fn skip_digit(it: &mut Chars) -> Option<char> {
|
||||
let mut c: Option<char>;
|
||||
loop {
|
||||
c = it.next();
|
||||
if !c.is_some() || !(c.unwrap() == '-' || c.unwrap() == '+' || c.unwrap().is_digit(10)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
c
|
||||
}
|
||||
|
||||
fn decode_header(content: &String) -> PlaylistItemHeader {
|
||||
let mut plih = PlaylistItemHeader {
|
||||
id: String::from(""),
|
||||
name: String::from(""),
|
||||
logo: String::from(""),
|
||||
logo_small: String::from(""),
|
||||
group: String::from("Unknown"),
|
||||
title: String::from(""),
|
||||
parent_code: String::from(""),
|
||||
audio_track: String::from(""),
|
||||
time_shift: String::from(""),
|
||||
rec: String::from(""),
|
||||
source: String::from(content),
|
||||
};
|
||||
|
||||
let mut it = content.chars();
|
||||
if token_till(&mut it, ':') == Some(String::from("#EXTINF")) {
|
||||
let mut c = skip_digit(&mut it);
|
||||
loop {
|
||||
if !c.is_some() {
|
||||
break;
|
||||
}
|
||||
match c.unwrap() {
|
||||
',' => plih.title = get_value(&mut it),
|
||||
_ => {
|
||||
let token = token_till(&mut it, '=');
|
||||
if token != None {
|
||||
let value = token_value(&mut it);
|
||||
match token.unwrap().as_str() {
|
||||
"tvg-id" => plih.id = value,
|
||||
"tvg-name" => plih.name = value,
|
||||
"group-title" => if !value.is_empty() { plih.group = value },
|
||||
"parent-code" => plih.parent_code = value,
|
||||
"audio-track" => plih.audio_track = value,
|
||||
"tvg-logo" => plih.logo = value,
|
||||
"tvg-logo-small" => plih.logo_small = value,
|
||||
"timeshift" => plih.time_shift = value,
|
||||
"tvg-rec" => plih.rec = value,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
c = it.next();
|
||||
}
|
||||
}
|
||||
plih
|
||||
}
|
||||
|
||||
pub fn decode(lines: &Vec<String>) -> Vec<PlaylistGroup> {
|
||||
let mut groups: HashMap<String, Vec<PlaylistItem>> = HashMap::new();
|
||||
let mut sort_order: Vec<String> = vec![];
|
||||
let mut header: Option<String> = None;
|
||||
|
||||
for line in lines {
|
||||
if line.starts_with("#EXTINF") {
|
||||
header = Some(String::from(line));
|
||||
continue;
|
||||
}
|
||||
if header.is_some() {
|
||||
let item = PlaylistItem { header: decode_header(&header.unwrap()), url: String::from(line) };
|
||||
let key = String::from(&item.header.group);
|
||||
let key2 = String::from(&item.header.group);
|
||||
match groups.entry(key) {
|
||||
Entry::Vacant(e) => {
|
||||
e.insert(vec![item]);
|
||||
sort_order.push(key2);
|
||||
}
|
||||
Entry::Occupied(mut e) => { e.get_mut().push(item); }
|
||||
}
|
||||
}
|
||||
header = None;
|
||||
}
|
||||
|
||||
let mut result: Vec<PlaylistGroup> = vec![];
|
||||
for (key, value) in groups {
|
||||
result.push(PlaylistGroup { title: key, channels: value });
|
||||
}
|
||||
result.sort_by(|f, s| {
|
||||
let i1 = sort_order.iter().position(|r| r == &f.title).unwrap();
|
||||
let i2 = sort_order.iter().position(|r| r == &s.title).unwrap();
|
||||
return i1.cmp(&i2);
|
||||
});
|
||||
result
|
||||
}
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
use std::io::BufRead;
|
||||
|
||||
mod m3u;
|
||||
mod config;
|
||||
mod processor;
|
||||
|
||||
fn open_file(file_name: &str) -> std::fs::File {
|
||||
let file = match std::fs::File::open(file_name) {
|
||||
Ok(file) => file,
|
||||
Err(_) => {
|
||||
println!("cant open file: {}", file_name);
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
file
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let matches = clap::App::new("m3u-filter")
|
||||
.version("0.1.0")
|
||||
.author("euzu")
|
||||
.about("Extended M3U playlist filter")
|
||||
.arg(clap::Arg::with_name("config")
|
||||
.short("c")
|
||||
.long("config")
|
||||
.takes_value(true)
|
||||
.help("the config file"))
|
||||
.arg(clap::Arg::with_name("input")
|
||||
.short("i")
|
||||
.long("input")
|
||||
.takes_value(true)
|
||||
.help("Input filename, overrides config input"))
|
||||
.get_matches();
|
||||
|
||||
let default_path = get_default_config_path();
|
||||
let config_file = matches.value_of("config").unwrap_or(default_path.as_str());
|
||||
|
||||
let mut cfg: config::Config = match serde_yaml::from_reader(open_file(config_file)) {
|
||||
Ok(result) => result,
|
||||
Err(e) => {
|
||||
println!("cant read config file: {}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
cfg.prepare();
|
||||
let file_name = matches.value_of("input").unwrap_or(if cfg.input.filename.is_empty() { "playlist.m3u" } else { cfg.input.filename.as_str() });
|
||||
let reader: Vec<String> = std::io::BufReader::new(open_file(file_name)).lines().map(|l| l.expect("Could not parse line")).collect();
|
||||
let result = m3u::decode(&reader);
|
||||
|
||||
processor::write_m3u(&result, &cfg);
|
||||
}
|
||||
|
||||
fn get_default_config_path() -> String {
|
||||
let default_path = std::path::Path::new("./");
|
||||
let current_exe = std::env::current_exe();
|
||||
let path: &std::path::Path = match current_exe {
|
||||
Ok(ref exe) => exe.parent().unwrap_or(default_path),
|
||||
Err(_) => default_path
|
||||
};
|
||||
let config_path = path.join("config.yml");
|
||||
String::from(if config_path.exists() {
|
||||
config_path.to_str().unwrap_or("./config.yml")
|
||||
} else {
|
||||
"./config.yml"
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
|
||||
use crate::{config, m3u};
|
||||
use crate::m3u::PlaylistItem;
|
||||
|
||||
pub fn write_m3u(playlist: &Vec<m3u::PlaylistGroup>, cfg: &config::Config) {
|
||||
for t in &cfg.targets {
|
||||
let mut file = match File::create(&t.filename) {
|
||||
Ok(file) => file,
|
||||
Err(_) => {
|
||||
println!("cant open file: {}", t.filename);
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
file.write(b"#EXTM3U\n").expect("Unable to write file");
|
||||
for pg in playlist {
|
||||
for pli in &pg.channels {
|
||||
if is_valid(&pli, &t.filter) {
|
||||
let content = exec_rename(&pli, &t.rename).map_or_else(|| pli.to_m3u(), |p| p.to_m3u());
|
||||
file.write(content.as_bytes()).expect("Unable to write file");
|
||||
file.write(b"\n").expect("Unable to write file");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_field_value<'a>(pli: &'a m3u::PlaylistItem, field: &config::ItemField) -> &'a str {
|
||||
let value = match field {
|
||||
config::ItemField::Group => pli.header.group.as_str(),
|
||||
config::ItemField::Name => pli.header.name.as_str(),
|
||||
config::ItemField::Title => pli.header.title.as_str(),
|
||||
};
|
||||
value
|
||||
}
|
||||
|
||||
fn set_field_value(pli: &mut m3u::PlaylistItem, field: &config::ItemField, value: String) -> () {
|
||||
let header = &mut pli.header;
|
||||
match field {
|
||||
config::ItemField::Group => header.group = value,
|
||||
config::ItemField::Name => header.name = value,
|
||||
config::ItemField::Title => header.title = value,
|
||||
};
|
||||
}
|
||||
|
||||
fn is_valid(pli: &m3u::PlaylistItem, filter: &config::ConfigFilters) -> bool {
|
||||
let mut matched = false;
|
||||
for r in &filter.rules {
|
||||
let value = get_field_value(pli, &r.field);
|
||||
matched = r.re.as_ref().unwrap().is_match(value);
|
||||
if matched {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return if filter.is_include() {
|
||||
matched
|
||||
} else {
|
||||
!matched
|
||||
};
|
||||
}
|
||||
|
||||
fn exec_rename(pli: &m3u::PlaylistItem, rename: &Vec<config::ConfigRename>) -> Option<PlaylistItem> {
|
||||
if rename.len() > 0 {
|
||||
let mut result = pli.clone();
|
||||
for r in rename {
|
||||
let value = get_field_value(pli, &r.field);
|
||||
let cap = r.re.as_ref().unwrap().replace_all(value, &r.new_name);
|
||||
let value = cap.into_owned();
|
||||
set_field_value(&mut result, &r.field, value);
|
||||
}
|
||||
return Some(result);
|
||||
}
|
||||
None
|
||||
}
|
||||
Reference in New Issue
Block a user