Merge pull request #239 from jb-alvarado/master

This commit is contained in:
jb-alvarado 2022-11-20 20:23:18 +01:00 committed by GitHub
commit dc3a8b68e2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 638 additions and 320 deletions

132
Cargo.lock generated
View File

@ -217,7 +217,7 @@ dependencies = [
"serde_urlencoded", "serde_urlencoded",
"smallvec", "smallvec",
"socket2", "socket2",
"time 0.3.16", "time 0.3.17",
"url", "url",
] ]
@ -341,22 +341,22 @@ version = "1.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e14485364214912d3b19cc3435dde4df66065127f05fa0d75c712f36f12c2f28" checksum = "e14485364214912d3b19cc3435dde4df66065127f05fa0d75c712f36f12c2f28"
dependencies = [ dependencies = [
"concurrent-queue", "concurrent-queue 1.2.4",
"event-listener", "event-listener",
"futures-core", "futures-core",
] ]
[[package]] [[package]]
name = "async-executor" name = "async-executor"
version = "1.4.1" version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "871f9bb5e0a22eeb7e8cf16641feb87c9dc67032ccf8ff49e772eb9941d3a965" checksum = "17adb73da160dfb475c183343c8cccd80721ea5a605d3eb57125f0a7b7a92d0b"
dependencies = [ dependencies = [
"async-lock",
"async-task", "async-task",
"concurrent-queue", "concurrent-queue 2.0.0",
"fastrand", "fastrand",
"futures-lite", "futures-lite",
"once_cell",
"slab", "slab",
] ]
@ -383,7 +383,7 @@ checksum = "e8121296a9f05be7f34aa4196b1747243b3b62e048bb7906f644f3fbfc490cf7"
dependencies = [ dependencies = [
"async-lock", "async-lock",
"autocfg", "autocfg",
"concurrent-queue", "concurrent-queue 1.2.4",
"futures-lite", "futures-lite",
"libc", "libc",
"log", "log",
@ -451,9 +451,9 @@ dependencies = [
[[package]] [[package]]
name = "asynchronous-codec" name = "asynchronous-codec"
version = "0.6.0" version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0de5164e5edbf51c45fb8c2d9664ae1c095cce1b265ecf7569093c0d66ef690" checksum = "06a0daa378f5fd10634e44b0a29b2a87b890657658e072a30d6f26e57ddee182"
dependencies = [ dependencies = [
"bytes", "bytes",
"futures-sink", "futures-sink",
@ -514,9 +514,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]] [[package]]
name = "blake2" name = "blake2"
version = "0.10.4" version = "0.10.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9cf849ee05b2ee5fba5e36f97ff8ec2533916700fc0758d40d92136a42f3388" checksum = "b12e5fd123190ce1c2e559308a94c9bacad77907d4c6005d9e58fe1a0689e55e"
dependencies = [ dependencies = [
"digest", "digest",
] ]
@ -594,9 +594,9 @@ checksum = "ec8a7b6a70fde80372154c65702f00a0f56f3e1c36abbc6c440484be248856db"
[[package]] [[package]]
name = "bytestring" name = "bytestring"
version = "1.1.0" version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86b6a75fd3048808ef06af5cd79712be8111960adaf89d90250974b38fc3928a" checksum = "f7f83e57d9154148e355404702e2694463241880b939570d7c97c014da7a69a1"
dependencies = [ dependencies = [
"bytes", "bytes",
] ]
@ -609,9 +609,9 @@ checksum = "c1db59621ec70f09c5e9b597b220c7a2b43611f4710dc03ceb8748637775692c"
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.0.74" version = "1.0.76"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "581f5dba903aac52ea3feb5ec4810848460ee833876f1f9b0fdeab1f19091574" checksum = "76a284da2e6fe2092f2353e51713435363112dfd60030e22add80be333fb928f"
dependencies = [ dependencies = [
"jobserver", "jobserver",
] ]
@ -630,9 +630,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]] [[package]]
name = "chrono" name = "chrono"
version = "0.4.22" version = "0.4.23"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfd4d1b31faaa3a89d7934dbded3111da0d2ef28e3ebccdb4f0179f5929d1ef1" checksum = "16b0a3d9ed01224b22057780a37bb8c5dbfe1be8ba48678e7bf57ec4b385411f"
dependencies = [ dependencies = [
"iana-time-zone", "iana-time-zone",
"js-sys", "js-sys",
@ -701,6 +701,15 @@ dependencies = [
"cache-padded", "cache-padded",
] ]
[[package]]
name = "concurrent-queue"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd7bef69dc86e3c610e4e7aed41035e2a7ed12e72dd7530f61327a6579a4390b"
dependencies = [
"crossbeam-utils",
]
[[package]] [[package]]
name = "convert_case" name = "convert_case"
version = "0.4.0" version = "0.4.0"
@ -714,7 +723,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "344adc371239ef32293cb1c4fe519592fcf21206c79c02854320afcdf3ab4917" checksum = "344adc371239ef32293cb1c4fe519592fcf21206c79c02854320afcdf3ab4917"
dependencies = [ dependencies = [
"percent-encoding", "percent-encoding",
"time 0.3.16", "time 0.3.17",
"version_check", "version_check",
] ]
@ -818,9 +827,9 @@ dependencies = [
[[package]] [[package]]
name = "cxx" name = "cxx"
version = "1.0.80" version = "1.0.82"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b7d4e43b25d3c994662706a1d4fcfc32aaa6afd287502c111b237093bb23f3a" checksum = "d4a41a86530d0fe7f5d9ea779916b7cadd2d4f9add748b99c2c029cbbdfaf453"
dependencies = [ dependencies = [
"cc", "cc",
"cxxbridge-flags", "cxxbridge-flags",
@ -830,9 +839,9 @@ dependencies = [
[[package]] [[package]]
name = "cxx-build" name = "cxx-build"
version = "1.0.80" version = "1.0.82"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84f8829ddc213e2c1368e51a2564c552b65a8cb6a28f31e576270ac81d5e5827" checksum = "06416d667ff3e3ad2df1cd8cd8afae5da26cf9cec4d0825040f88b5ca659a2f0"
dependencies = [ dependencies = [
"cc", "cc",
"codespan-reporting", "codespan-reporting",
@ -845,15 +854,15 @@ dependencies = [
[[package]] [[package]]
name = "cxxbridge-flags" name = "cxxbridge-flags"
version = "1.0.80" version = "1.0.82"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e72537424b474af1460806647c41d4b6d35d09ef7fe031c5c2fa5766047cc56a" checksum = "820a9a2af1669deeef27cb271f476ffd196a2c4b6731336011e0ba63e2c7cf71"
[[package]] [[package]]
name = "cxxbridge-macro" name = "cxxbridge-macro"
version = "1.0.80" version = "1.0.82"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "309e4fb93eed90e1e14bea0da16b209f81813ba9fc7830c20ed151dd7bc0a4d7" checksum = "a08a6e2fcc370a089ad3b4aaf54db3b1b4cee38ddabce5896b33eb693275f470"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -888,9 +897,9 @@ dependencies = [
[[package]] [[package]]
name = "digest" name = "digest"
version = "0.10.5" version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adfbc57365a37acbd2ebf2b64d7e69bb766e2fea813521ed536f5d0520dcf86c" checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f"
dependencies = [ dependencies = [
"block-buffer", "block-buffer",
"crypto-common", "crypto-common",
@ -962,7 +971,7 @@ dependencies = [
[[package]] [[package]]
name = "ffplayout" name = "ffplayout"
version = "0.16.3" version = "0.16.4"
dependencies = [ dependencies = [
"chrono", "chrono",
"clap", "clap",
@ -982,7 +991,7 @@ dependencies = [
[[package]] [[package]]
name = "ffplayout-api" name = "ffplayout-api"
version = "0.7.1" version = "0.8.0"
dependencies = [ dependencies = [
"actix-files", "actix-files",
"actix-multipart", "actix-multipart",
@ -999,6 +1008,7 @@ dependencies = [
"jsonwebtoken", "jsonwebtoken",
"once_cell", "once_cell",
"rand", "rand",
"regex",
"relative-path", "relative-path",
"reqwest", "reqwest",
"rpassword", "rpassword",
@ -1012,7 +1022,7 @@ dependencies = [
[[package]] [[package]]
name = "ffplayout-lib" name = "ffplayout-lib"
version = "0.16.3" version = "0.16.4"
dependencies = [ dependencies = [
"chrono", "chrono",
"crossbeam-channel", "crossbeam-channel",
@ -1031,7 +1041,7 @@ dependencies = [
"serde_yaml", "serde_yaml",
"shlex", "shlex",
"simplelog", "simplelog",
"time 0.3.16", "time 0.3.17",
"walkdir", "walkdir",
"winapi 0.3.9", "winapi 0.3.9",
] ]
@ -1430,9 +1440,9 @@ checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421"
[[package]] [[package]]
name = "hyper" name = "hyper"
version = "0.14.22" version = "0.14.23"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "abfba89e19b959ca163c7752ba59d737c1ceea53a5d31a149c805446fc958064" checksum = "034711faac9d2166cb1baf1a2fb0b60b1f277f8492fd72176c17f3515e1abd3c"
dependencies = [ dependencies = [
"bytes", "bytes",
"futures-channel", "futures-channel",
@ -1512,9 +1522,9 @@ dependencies = [
[[package]] [[package]]
name = "indexmap" name = "indexmap"
version = "1.9.1" version = "1.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399"
dependencies = [ dependencies = [
"autocfg", "autocfg",
"hashbrown", "hashbrown",
@ -1560,9 +1570,9 @@ dependencies = [
[[package]] [[package]]
name = "ipnet" name = "ipnet"
version = "2.5.0" version = "2.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "879d54834c8c76457ef4293a689b2a8c59b076067ad77b15efafbb05f92a592b" checksum = "f88c5561171189e69df9d98bcf18fd5f9558300f7ea7b801eb8a0fd748bd8745"
[[package]] [[package]]
name = "itertools" name = "itertools"
@ -2062,9 +2072,9 @@ dependencies = [
[[package]] [[package]]
name = "os_str_bytes" name = "os_str_bytes"
version = "6.3.1" version = "6.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3baf96e39c5359d2eb0dd6ccb42c62b91d9678aa68160d261b9e0ccbf9e9dea9" checksum = "7b5bf27447411e9ee3ff51186bf7a08e16c341efdde93f4d823e8844429bed7e"
[[package]] [[package]]
name = "paris" name = "paris"
@ -2212,9 +2222,9 @@ dependencies = [
[[package]] [[package]]
name = "ppv-lite86" name = "ppv-lite86"
version = "0.2.16" version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
[[package]] [[package]]
name = "proc-macro-error" name = "proc-macro-error"
@ -2305,9 +2315,9 @@ dependencies = [
[[package]] [[package]]
name = "regex" name = "regex"
version = "1.6.0" version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b" checksum = "e076559ef8e241f2ae3479e36f97bd5741c0330689e217ad51ce2c76808b868a"
dependencies = [ dependencies = [
"aho-corasick", "aho-corasick",
"memchr", "memchr",
@ -2316,9 +2326,9 @@ dependencies = [
[[package]] [[package]]
name = "regex-syntax" name = "regex-syntax"
version = "0.6.27" version = "0.6.28"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244" checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848"
[[package]] [[package]]
name = "relative-path" name = "relative-path"
@ -2337,9 +2347,9 @@ dependencies = [
[[package]] [[package]]
name = "reqwest" name = "reqwest"
version = "0.11.12" version = "0.11.13"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "431949c384f4e2ae07605ccaa56d1d9d2ecdb5cadd4f9577ccfab29f2e5149fc" checksum = "68cc60575865c7831548863cc02356512e3f1dc2f3f82cb837d7fc4cc8f3c97c"
dependencies = [ dependencies = [
"base64", "base64",
"bytes", "bytes",
@ -2506,9 +2516,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.87" version = "1.0.88"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ce777b7b150d76b9cf60d28b55f5847135a003f7d7350c6be7a773508ce7d45" checksum = "8e8b3801309262e8184d9687fb697586833e939767aea0dda89f5a8e650e8bd7"
dependencies = [ dependencies = [
"itoa", "itoa",
"ryu", "ryu",
@ -2585,7 +2595,7 @@ dependencies = [
"num-bigint", "num-bigint",
"num-traits", "num-traits",
"thiserror", "thiserror",
"time 0.3.16", "time 0.3.17",
] ]
[[package]] [[package]]
@ -2597,7 +2607,7 @@ dependencies = [
"log", "log",
"paris", "paris",
"termcolor", "termcolor",
"time 0.3.16", "time 0.3.17",
] ]
[[package]] [[package]]
@ -2813,7 +2823,7 @@ dependencies = [
"serde_yaml", "serde_yaml",
"shlex", "shlex",
"simplelog", "simplelog",
"time 0.3.16", "time 0.3.17",
"walkdir", "walkdir",
] ]
@ -2856,9 +2866,9 @@ dependencies = [
[[package]] [[package]]
name = "time" name = "time"
version = "0.3.16" version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fab5c8b9980850e06d92ddbe3ab839c062c801f3927c0fb8abd6fc8e918fbca" checksum = "a561bf4617eebd33bca6434b988f39ed798e527f51a1e797d0ee4f61c0a38376"
dependencies = [ dependencies = [
"itoa", "itoa",
"libc", "libc",
@ -2876,9 +2886,9 @@ checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd"
[[package]] [[package]]
name = "time-macros" name = "time-macros"
version = "0.2.5" version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65bb801831d812c562ae7d2bfb531f26e66e4e1f6b17307ba4149c5064710e5b" checksum = "d967f99f534ca7e495c575c62638eebc2898a8c84c119b89e250477bc4ba16b2"
dependencies = [ dependencies = [
"time-core", "time-core",
] ]
@ -2900,9 +2910,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
[[package]] [[package]]
name = "tokio" name = "tokio"
version = "1.21.2" version = "1.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9e03c497dc955702ba729190dc4aac6f2a0ce97f913e5b1b5912fc5039d9099" checksum = "d76ce4a75fb488c605c54bf610f221cea8b0dafb53333c1a67e8ee199dcd2ae3"
dependencies = [ dependencies = [
"autocfg", "autocfg",
"bytes", "bytes",
@ -3088,9 +3098,9 @@ dependencies = [
[[package]] [[package]]
name = "uuid" name = "uuid"
version = "1.2.1" version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "feb41e78f93363bb2df8b0e86a2ca30eed7806ea16ea0c790d757cf93f79be83" checksum = "422ee0de9031b5b948b97a8fc04e3aa35230001a722ddd27943e0be31564ce4c"
dependencies = [ dependencies = [
"getrandom", "getrandom",
] ]

View File

@ -20,7 +20,7 @@ Check the [releases](https://github.com/ffplayout/ffplayout/releases/latest) for
- send emails with error message - send emails with error message
- overlay a logo - overlay a logo
- overlay text, controllable through [ffplayout-frontend](https://github.com/ffplayout/ffplayout-frontend) (needs ffmpeg with libzmq and enabled JSON RPC server) - overlay text, controllable through [ffplayout-frontend](https://github.com/ffplayout/ffplayout-frontend) (needs ffmpeg with libzmq and enabled JSON RPC server)
- EBU R128 loudness normalization (single pass) - EBU R128 loudness normalization (single pass) (experimental *)
- loop playlist infinitely - loop playlist infinitely
- [remote source](/docs/remote_source.md) - [remote source](/docs/remote_source.md)
- trim and fade the last clip, to get full 24 hours - trim and fade the last clip, to get full 24 hours

View File

@ -284,7 +284,7 @@ curl -X DELETE http://127.0.0.1:8787/api/playlist/1/2022-06-20
**Read Log Life** **Read Log Life**
```BASH ```BASH
curl -X Get http://127.0.0.1:8787/api/log/1 curl -X GET http://127.0.0.1:8787/api/log/1
-H 'Content-Type: application/json' -H 'Authorization: Bearer <TOKEN>' -H 'Content-Type: application/json' -H 'Authorization: Bearer <TOKEN>'
``` ```
@ -321,7 +321,7 @@ curl -X POST http://127.0.0.1:8787/api/file/1/remove/ -H 'Content-Type: applicat
**Upload File** **Upload File**
```BASH ```BASH
curl -X POST http://127.0.0.1:8787/api/file/1/upload/ -H 'Authorization: Bearer <TOKEN>' curl -X PUT http://127.0.0.1:8787/api/file/1/upload/ -H 'Authorization: Bearer <TOKEN>'
-F "file=@file.mp4" -F "file=@file.mp4"
``` ```
@ -331,6 +331,30 @@ Import text/m3u file and convert it to a playlist
lines with leading "#" will be ignore lines with leading "#" will be ignore
```BASH ```BASH
curl -X POST http://127.0.0.1:8787/api/file/1/import/ -H 'Authorization: Bearer <TOKEN>' curl -X PUT http://127.0.0.1:8787/api/file/1/import/ -H 'Authorization: Bearer <TOKEN>'
-F "file=@list.m3u" -F "file=@list.m3u"
``` ```
**Program info**
Get program infos about given date, or current day
Examples:
* get program from current day
```BASH
curl -X GET http://127.0.0.1:8787/api/program/1/ -H 'Authorization: Bearer <TOKEN>'
```
* get a program range between two dates
```BASH
curl -X GET http://127.0.0.1:8787/api/program/1/?start_after=2022-11-13T12:00:00&start_before=2022-11-20T11:59:59 \
-H 'Authorization: Bearer <TOKEN>'
```
* get program from give day
```BASH
curl -X GET http://127.0.0.1:8787/api/program/1/?start_after=2022-11-13T10:00:00 \
-H 'Authorization: Bearer <TOKEN>'
```

View File

@ -4,7 +4,7 @@ description = "Rest API for ffplayout"
license = "GPL-3.0" license = "GPL-3.0"
authors = ["Jonathan Baecker jonbae77@gmail.com"] authors = ["Jonathan Baecker jonbae77@gmail.com"]
readme = "README.md" readme = "README.md"
version = "0.7.1" version = "0.8.0"
edition = "2021" edition = "2021"
[dependencies] [dependencies]
@ -23,6 +23,7 @@ futures-util = { version = "0.3", default-features = false, features = ["std"] }
jsonwebtoken = "8" jsonwebtoken = "8"
once_cell = "1.10" once_cell = "1.10"
rand = "0.8" rand = "0.8"
regex = "1"
relative-path = "1.6" relative-path = "1.6"
reqwest = { version = "0.11", features = ["blocking", "json"] } reqwest = { version = "0.11", features = ["blocking", "json"] }
rpassword = "6.0" rpassword = "6.0"

View File

@ -17,8 +17,11 @@ use argon2::{
password_hash::{rand_core::OsRng, PasswordHash, SaltString}, password_hash::{rand_core::OsRng, PasswordHash, SaltString},
Argon2, PasswordHasher, PasswordVerifier, Argon2, PasswordHasher, PasswordVerifier,
}; };
use chrono::{DateTime, Datelike, Duration, Local, NaiveDateTime, TimeZone, Utc};
use regex::Regex;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use simplelog::*; use simplelog::*;
use sqlx::{Pool, Sqlite};
use crate::auth::{create_jwt, Claims}; use crate::auth::{create_jwt, Claims};
use crate::db::{ use crate::db::{
@ -33,10 +36,16 @@ use crate::utils::{
browser, create_directory, remove_file_or_folder, rename_file, upload, MoveObject, browser, create_directory, remove_file_or_folder, rename_file, upload, MoveObject,
PathObject, PathObject,
}, },
naive_date_time_from_str,
playlist::{delete_playlist, generate_playlist, read_playlist, write_playlist}, playlist::{delete_playlist, generate_playlist, read_playlist, write_playlist},
playout_config, read_log_file, read_playout_config, Role, playout_config, read_log_file, read_playout_config, Role,
}; };
use ffplayout_lib::utils::{import::import_file, JsonPlaylist, PlayoutConfig}; use ffplayout_lib::{
utils::{
get_date_range, import::import_file, sec_to_time, time_to_sec, JsonPlaylist, PlayoutConfig,
},
vec_strings,
};
#[derive(Serialize)] #[derive(Serialize)]
struct ResponseObj<T> { struct ResponseObj<T> {
@ -71,6 +80,42 @@ pub struct ImportObj {
date: String, date: String,
} }
#[derive(Debug, Deserialize, Clone)]
pub struct ProgramObj {
#[serde(default = "time_after", deserialize_with = "naive_date_time_from_str")]
start_after: NaiveDateTime,
#[serde(default = "time_before", deserialize_with = "naive_date_time_from_str")]
start_before: NaiveDateTime,
}
fn time_after() -> NaiveDateTime {
let today = Utc::now();
chrono::Local
.with_ymd_and_hms(today.year(), today.month(), today.day(), 0, 0, 0)
.unwrap()
.naive_local()
}
fn time_before() -> NaiveDateTime {
let today = Utc::now();
chrono::Local
.with_ymd_and_hms(today.year(), today.month(), today.day(), 23, 59, 59)
.unwrap()
.naive_local()
}
#[derive(Debug, Serialize)]
struct ProgramItem {
source: String,
start: String,
r#in: f64,
out: f64,
duration: f64,
category: String,
}
/// #### User Handling /// #### User Handling
/// ///
/// **Login** /// **Login**
@ -90,8 +135,9 @@ pub struct ImportObj {
/// } /// }
/// ``` /// ```
#[post("/auth/login/")] #[post("/auth/login/")]
pub async fn login(credentials: web::Json<User>) -> impl Responder { pub async fn login(pool: web::Data<Pool<Sqlite>>, credentials: web::Json<User>) -> impl Responder {
match handles::select_login(&credentials.username).await { let conn = pool.into_inner();
match handles::select_login(&conn, &credentials.username).await {
Ok(mut user) => { Ok(mut user) => {
let pass = user.password.clone(); let pass = user.password.clone();
let hash = PasswordHash::new(&pass).unwrap(); let hash = PasswordHash::new(&pass).unwrap();
@ -102,7 +148,7 @@ pub async fn login(credentials: web::Json<User>) -> impl Responder {
.verify_password(credentials.password.as_bytes(), &hash) .verify_password(credentials.password.as_bytes(), &hash)
.is_ok() .is_ok()
{ {
let role = handles::select_role(&user.role_id.unwrap_or_default()) let role = handles::select_role(&conn, &user.role_id.unwrap_or_default())
.await .await
.unwrap_or_else(|_| "guest".to_string()); .unwrap_or_else(|_| "guest".to_string());
let claims = Claims::new(user.id, user.username.clone(), role.clone()); let claims = Claims::new(user.id, user.username.clone(), role.clone());
@ -152,8 +198,11 @@ pub async fn login(credentials: web::Json<User>) -> impl Responder {
/// ``` /// ```
#[get("/user")] #[get("/user")]
#[has_any_role("Role::Admin", "Role::User", type = "Role")] #[has_any_role("Role::Admin", "Role::User", type = "Role")]
async fn get_user(user: web::ReqData<LoginUser>) -> Result<impl Responder, ServiceError> { async fn get_user(
match handles::select_user(&user.username).await { pool: web::Data<Pool<Sqlite>>,
user: web::ReqData<LoginUser>,
) -> Result<impl Responder, ServiceError> {
match handles::select_user(&pool.into_inner(), &user.username).await {
Ok(user) => Ok(web::Json(user)), Ok(user) => Ok(web::Json(user)),
Err(e) => { Err(e) => {
error!("{e}"); error!("{e}");
@ -171,6 +220,7 @@ async fn get_user(user: web::ReqData<LoginUser>) -> Result<impl Responder, Servi
#[put("/user/{id}")] #[put("/user/{id}")]
#[has_any_role("Role::Admin", "Role::User", type = "Role")] #[has_any_role("Role::Admin", "Role::User", type = "Role")]
async fn update_user( async fn update_user(
pool: web::Data<Pool<Sqlite>>,
id: web::Path<i32>, id: web::Path<i32>,
user: web::ReqData<LoginUser>, user: web::ReqData<LoginUser>,
data: web::Json<User>, data: web::Json<User>,
@ -195,7 +245,10 @@ async fn update_user(
fields.push_str(format!("password = '{}', salt = '{salt}'", password_hash).as_str()); fields.push_str(format!("password = '{}', salt = '{salt}'", password_hash).as_str());
} }
if handles::update_user(user.id, fields).await.is_ok() { if handles::update_user(&pool.into_inner(), user.id, fields)
.await
.is_ok()
{
return Ok("Update Success"); return Ok("Update Success");
}; };
@ -214,8 +267,11 @@ async fn update_user(
/// ``` /// ```
#[post("/user/")] #[post("/user/")]
#[has_any_role("Role::Admin", type = "Role")] #[has_any_role("Role::Admin", type = "Role")]
async fn add_user(data: web::Json<User>) -> Result<impl Responder, ServiceError> { async fn add_user(
match handles::insert_user(data.into_inner()).await { pool: web::Data<Pool<Sqlite>>,
data: web::Json<User>,
) -> Result<impl Responder, ServiceError> {
match handles::insert_user(&pool.into_inner(), data.into_inner()).await {
Ok(_) => Ok("Add User Success"), Ok(_) => Ok("Add User Success"),
Err(e) => { Err(e) => {
error!("{e}"); error!("{e}");
@ -247,8 +303,11 @@ async fn add_user(data: web::Json<User>) -> Result<impl Responder, ServiceError>
/// ``` /// ```
#[get("/channel/{id}")] #[get("/channel/{id}")]
#[has_any_role("Role::Admin", "Role::User", type = "Role")] #[has_any_role("Role::Admin", "Role::User", type = "Role")]
async fn get_channel(id: web::Path<i32>) -> Result<impl Responder, ServiceError> { async fn get_channel(
if let Ok(channel) = handles::select_channel(&id).await { pool: web::Data<Pool<Sqlite>>,
id: web::Path<i32>,
) -> Result<impl Responder, ServiceError> {
if let Ok(channel) = handles::select_channel(&pool.into_inner(), &id).await {
return Ok(web::Json(channel)); return Ok(web::Json(channel));
} }
@ -262,8 +321,8 @@ async fn get_channel(id: web::Path<i32>) -> Result<impl Responder, ServiceError>
/// ``` /// ```
#[get("/channels")] #[get("/channels")]
#[has_any_role("Role::Admin", type = "Role")] #[has_any_role("Role::Admin", type = "Role")]
async fn get_all_channels() -> Result<impl Responder, ServiceError> { async fn get_all_channels(pool: web::Data<Pool<Sqlite>>) -> Result<impl Responder, ServiceError> {
if let Ok(channel) = handles::select_all_channels().await { if let Ok(channel) = handles::select_all_channels(&pool.into_inner()).await {
return Ok(web::Json(channel)); return Ok(web::Json(channel));
} }
@ -281,10 +340,11 @@ async fn get_all_channels() -> Result<impl Responder, ServiceError> {
#[patch("/channel/{id}")] #[patch("/channel/{id}")]
#[has_any_role("Role::Admin", type = "Role")] #[has_any_role("Role::Admin", type = "Role")]
async fn patch_channel( async fn patch_channel(
pool: web::Data<Pool<Sqlite>>,
id: web::Path<i32>, id: web::Path<i32>,
data: web::Json<Channel>, data: web::Json<Channel>,
) -> Result<impl Responder, ServiceError> { ) -> Result<impl Responder, ServiceError> {
if handles::update_channel(*id, data.into_inner()) if handles::update_channel(&pool.into_inner(), *id, data.into_inner())
.await .await
.is_ok() .is_ok()
{ {
@ -305,8 +365,11 @@ async fn patch_channel(
/// ``` /// ```
#[post("/channel/")] #[post("/channel/")]
#[has_any_role("Role::Admin", type = "Role")] #[has_any_role("Role::Admin", type = "Role")]
async fn add_channel(data: web::Json<Channel>) -> Result<impl Responder, ServiceError> { async fn add_channel(
match create_channel(data.into_inner()).await { pool: web::Data<Pool<Sqlite>>,
data: web::Json<Channel>,
) -> Result<impl Responder, ServiceError> {
match create_channel(&pool.into_inner(), data.into_inner()).await {
Ok(c) => Ok(web::Json(c)), Ok(c) => Ok(web::Json(c)),
Err(e) => Err(e), Err(e) => Err(e),
} }
@ -319,8 +382,11 @@ async fn add_channel(data: web::Json<Channel>) -> Result<impl Responder, Service
/// ``` /// ```
#[delete("/channel/{id}")] #[delete("/channel/{id}")]
#[has_any_role("Role::Admin", type = "Role")] #[has_any_role("Role::Admin", type = "Role")]
async fn remove_channel(id: web::Path<i32>) -> Result<impl Responder, ServiceError> { async fn remove_channel(
if delete_channel(*id).await.is_ok() { pool: web::Data<Pool<Sqlite>>,
id: web::Path<i32>,
) -> Result<impl Responder, ServiceError> {
if delete_channel(&pool.into_inner(), *id).await.is_ok() {
return Ok("Delete Channel Success"); return Ok("Delete Channel Success");
} }
@ -339,10 +405,11 @@ async fn remove_channel(id: web::Path<i32>) -> Result<impl Responder, ServiceErr
#[get("/playout/config/{id}")] #[get("/playout/config/{id}")]
#[has_any_role("Role::Admin", "Role::User", type = "Role")] #[has_any_role("Role::Admin", "Role::User", type = "Role")]
async fn get_playout_config( async fn get_playout_config(
pool: web::Data<Pool<Sqlite>>,
id: web::Path<i32>, id: web::Path<i32>,
_details: AuthDetails<Role>, _details: AuthDetails<Role>,
) -> Result<impl Responder, ServiceError> { ) -> Result<impl Responder, ServiceError> {
if let Ok(channel) = handles::select_channel(&id).await { if let Ok(channel) = handles::select_channel(&pool.into_inner(), &id).await {
if let Ok(config) = read_playout_config(&channel.config_path) { if let Ok(config) = read_playout_config(&channel.config_path) {
return Ok(web::Json(config)); return Ok(web::Json(config));
} }
@ -360,10 +427,11 @@ async fn get_playout_config(
#[put("/playout/config/{id}")] #[put("/playout/config/{id}")]
#[has_any_role("Role::Admin", type = "Role")] #[has_any_role("Role::Admin", type = "Role")]
async fn update_playout_config( async fn update_playout_config(
pool: web::Data<Pool<Sqlite>>,
id: web::Path<i32>, id: web::Path<i32>,
data: web::Json<PlayoutConfig>, data: web::Json<PlayoutConfig>,
) -> Result<impl Responder, ServiceError> { ) -> Result<impl Responder, ServiceError> {
if let Ok(channel) = handles::select_channel(&id).await { if let Ok(channel) = handles::select_channel(&pool.into_inner(), &id).await {
if let Ok(f) = std::fs::OpenOptions::new() if let Ok(f) = std::fs::OpenOptions::new()
.write(true) .write(true)
.truncate(true) .truncate(true)
@ -392,8 +460,11 @@ async fn update_playout_config(
/// ``` /// ```
#[get("/presets/{id}")] #[get("/presets/{id}")]
#[has_any_role("Role::Admin", "Role::User", type = "Role")] #[has_any_role("Role::Admin", "Role::User", type = "Role")]
async fn get_presets(id: web::Path<i32>) -> Result<impl Responder, ServiceError> { async fn get_presets(
if let Ok(presets) = handles::select_presets(*id).await { pool: web::Data<Pool<Sqlite>>,
id: web::Path<i32>,
) -> Result<impl Responder, ServiceError> {
if let Ok(presets) = handles::select_presets(&pool.into_inner(), *id).await {
return Ok(web::Json(presets)); return Ok(web::Json(presets));
} }
@ -411,10 +482,14 @@ async fn get_presets(id: web::Path<i32>) -> Result<impl Responder, ServiceError>
#[put("/presets/{id}")] #[put("/presets/{id}")]
#[has_any_role("Role::Admin", "Role::User", type = "Role")] #[has_any_role("Role::Admin", "Role::User", type = "Role")]
async fn update_preset( async fn update_preset(
pool: web::Data<Pool<Sqlite>>,
id: web::Path<i32>, id: web::Path<i32>,
data: web::Json<TextPreset>, data: web::Json<TextPreset>,
) -> Result<impl Responder, ServiceError> { ) -> Result<impl Responder, ServiceError> {
if handles::update_preset(&id, data.into_inner()).await.is_ok() { if handles::update_preset(&pool.into_inner(), &id, data.into_inner())
.await
.is_ok()
{
return Ok("Update Success"); return Ok("Update Success");
} }
@ -431,8 +506,14 @@ async fn update_preset(
/// ``` /// ```
#[post("/presets/")] #[post("/presets/")]
#[has_any_role("Role::Admin", "Role::User", type = "Role")] #[has_any_role("Role::Admin", "Role::User", type = "Role")]
async fn add_preset(data: web::Json<TextPreset>) -> Result<impl Responder, ServiceError> { async fn add_preset(
if handles::insert_preset(data.into_inner()).await.is_ok() { pool: web::Data<Pool<Sqlite>>,
data: web::Json<TextPreset>,
) -> Result<impl Responder, ServiceError> {
if handles::insert_preset(&pool.into_inner(), data.into_inner())
.await
.is_ok()
{
return Ok("Add preset Success"); return Ok("Add preset Success");
} }
@ -447,8 +528,14 @@ async fn add_preset(data: web::Json<TextPreset>) -> Result<impl Responder, Servi
/// ``` /// ```
#[delete("/presets/{id}")] #[delete("/presets/{id}")]
#[has_any_role("Role::Admin", "Role::User", type = "Role")] #[has_any_role("Role::Admin", "Role::User", type = "Role")]
async fn delete_preset(id: web::Path<i32>) -> Result<impl Responder, ServiceError> { async fn delete_preset(
if handles::delete_preset(&id).await.is_ok() { pool: web::Data<Pool<Sqlite>>,
id: web::Path<i32>,
) -> Result<impl Responder, ServiceError> {
if handles::delete_preset(&pool.into_inner(), &id)
.await
.is_ok()
{
return Ok("Delete preset Success"); return Ok("Delete preset Success");
} }
@ -475,10 +562,11 @@ async fn delete_preset(id: web::Path<i32>) -> Result<impl Responder, ServiceErro
#[post("/control/{id}/text/")] #[post("/control/{id}/text/")]
#[has_any_role("Role::Admin", "Role::User", type = "Role")] #[has_any_role("Role::Admin", "Role::User", type = "Role")]
pub async fn send_text_message( pub async fn send_text_message(
pool: web::Data<Pool<Sqlite>>,
id: web::Path<i32>, id: web::Path<i32>,
data: web::Json<HashMap<String, String>>, data: web::Json<HashMap<String, String>>,
) -> Result<impl Responder, ServiceError> { ) -> Result<impl Responder, ServiceError> {
match send_message(*id, data.into_inner()).await { match send_message(&pool.into_inner(), *id, data.into_inner()).await {
Ok(res) => Ok(res.text().await.unwrap_or_else(|_| "Success".into())), Ok(res) => Ok(res.text().await.unwrap_or_else(|_| "Success".into())),
Err(e) => Err(e), Err(e) => Err(e),
} }
@ -497,10 +585,11 @@ pub async fn send_text_message(
#[post("/control/{id}/playout/")] #[post("/control/{id}/playout/")]
#[has_any_role("Role::Admin", "Role::User", type = "Role")] #[has_any_role("Role::Admin", "Role::User", type = "Role")]
pub async fn control_playout( pub async fn control_playout(
pool: web::Data<Pool<Sqlite>>,
id: web::Path<i32>, id: web::Path<i32>,
control: web::Json<Process>, control: web::Json<Process>,
) -> Result<impl Responder, ServiceError> { ) -> Result<impl Responder, ServiceError> {
match control_state(*id, control.command.clone()).await { match control_state(&pool.into_inner(), *id, control.command.clone()).await {
Ok(res) => Ok(res.text().await.unwrap_or_else(|_| "Success".into())), Ok(res) => Ok(res.text().await.unwrap_or_else(|_| "Success".into())),
Err(e) => Err(e), Err(e) => Err(e),
} }
@ -538,8 +627,11 @@ pub async fn control_playout(
/// ``` /// ```
#[get("/control/{id}/media/current")] #[get("/control/{id}/media/current")]
#[has_any_role("Role::Admin", "Role::User", type = "Role")] #[has_any_role("Role::Admin", "Role::User", type = "Role")]
pub async fn media_current(id: web::Path<i32>) -> Result<impl Responder, ServiceError> { pub async fn media_current(
match media_info(*id, "current".into()).await { pool: web::Data<Pool<Sqlite>>,
id: web::Path<i32>,
) -> Result<impl Responder, ServiceError> {
match media_info(&pool.into_inner(), *id, "current".into()).await {
Ok(res) => Ok(res.text().await.unwrap_or_else(|_| "Success".into())), Ok(res) => Ok(res.text().await.unwrap_or_else(|_| "Success".into())),
Err(e) => Err(e), Err(e) => Err(e),
} }
@ -552,8 +644,11 @@ pub async fn media_current(id: web::Path<i32>) -> Result<impl Responder, Service
/// ``` /// ```
#[get("/control/{id}/media/next")] #[get("/control/{id}/media/next")]
#[has_any_role("Role::Admin", "Role::User", type = "Role")] #[has_any_role("Role::Admin", "Role::User", type = "Role")]
pub async fn media_next(id: web::Path<i32>) -> Result<impl Responder, ServiceError> { pub async fn media_next(
match media_info(*id, "next".into()).await { pool: web::Data<Pool<Sqlite>>,
id: web::Path<i32>,
) -> Result<impl Responder, ServiceError> {
match media_info(&pool.into_inner(), *id, "next".into()).await {
Ok(res) => Ok(res.text().await.unwrap_or_else(|_| "Success".into())), Ok(res) => Ok(res.text().await.unwrap_or_else(|_| "Success".into())),
Err(e) => Err(e), Err(e) => Err(e),
} }
@ -567,8 +662,11 @@ pub async fn media_next(id: web::Path<i32>) -> Result<impl Responder, ServiceErr
/// ``` /// ```
#[get("/control/{id}/media/last")] #[get("/control/{id}/media/last")]
#[has_any_role("Role::Admin", "Role::User", type = "Role")] #[has_any_role("Role::Admin", "Role::User", type = "Role")]
pub async fn media_last(id: web::Path<i32>) -> Result<impl Responder, ServiceError> { pub async fn media_last(
match media_info(*id, "last".into()).await { pool: web::Data<Pool<Sqlite>>,
id: web::Path<i32>,
) -> Result<impl Responder, ServiceError> {
match media_info(&pool.into_inner(), *id, "last".into()).await {
Ok(res) => Ok(res.text().await.unwrap_or_else(|_| "Success".into())), Ok(res) => Ok(res.text().await.unwrap_or_else(|_| "Success".into())),
Err(e) => Err(e), Err(e) => Err(e),
} }
@ -590,10 +688,11 @@ pub async fn media_last(id: web::Path<i32>) -> Result<impl Responder, ServiceErr
#[post("/control/{id}/process/")] #[post("/control/{id}/process/")]
#[has_any_role("Role::Admin", "Role::User", type = "Role")] #[has_any_role("Role::Admin", "Role::User", type = "Role")]
pub async fn process_control( pub async fn process_control(
pool: web::Data<Pool<Sqlite>>,
id: web::Path<i32>, id: web::Path<i32>,
proc: web::Json<Process>, proc: web::Json<Process>,
) -> Result<impl Responder, ServiceError> { ) -> Result<impl Responder, ServiceError> {
control_service(*id, &proc.command).await control_service(&pool.into_inner(), *id, &proc.command).await
} }
/// #### ffplayout Playlist Operations /// #### ffplayout Playlist Operations
@ -607,10 +706,11 @@ pub async fn process_control(
#[get("/playlist/{id}")] #[get("/playlist/{id}")]
#[has_any_role("Role::Admin", "Role::User", type = "Role")] #[has_any_role("Role::Admin", "Role::User", type = "Role")]
pub async fn get_playlist( pub async fn get_playlist(
pool: web::Data<Pool<Sqlite>>,
id: web::Path<i32>, id: web::Path<i32>,
obj: web::Query<DateObj>, obj: web::Query<DateObj>,
) -> Result<impl Responder, ServiceError> { ) -> Result<impl Responder, ServiceError> {
match read_playlist(*id, obj.date.clone()).await { match read_playlist(&pool.into_inner(), *id, obj.date.clone()).await {
Ok(playlist) => Ok(web::Json(playlist)), Ok(playlist) => Ok(web::Json(playlist)),
Err(e) => Err(e), Err(e) => Err(e),
} }
@ -626,10 +726,11 @@ pub async fn get_playlist(
#[post("/playlist/{id}/")] #[post("/playlist/{id}/")]
#[has_any_role("Role::Admin", "Role::User", type = "Role")] #[has_any_role("Role::Admin", "Role::User", type = "Role")]
pub async fn save_playlist( pub async fn save_playlist(
pool: web::Data<Pool<Sqlite>>,
id: web::Path<i32>, id: web::Path<i32>,
data: web::Json<JsonPlaylist>, data: web::Json<JsonPlaylist>,
) -> Result<impl Responder, ServiceError> { ) -> Result<impl Responder, ServiceError> {
match write_playlist(*id, data.into_inner()).await { match write_playlist(&pool.into_inner(), *id, data.into_inner()).await {
Ok(res) => Ok(res), Ok(res) => Ok(res),
Err(e) => Err(e), Err(e) => Err(e),
} }
@ -646,9 +747,10 @@ pub async fn save_playlist(
#[get("/playlist/{id}/generate/{date}")] #[get("/playlist/{id}/generate/{date}")]
#[has_any_role("Role::Admin", "Role::User", type = "Role")] #[has_any_role("Role::Admin", "Role::User", type = "Role")]
pub async fn gen_playlist( pub async fn gen_playlist(
pool: web::Data<Pool<Sqlite>>,
params: web::Path<(i32, String)>, params: web::Path<(i32, String)>,
) -> Result<impl Responder, ServiceError> { ) -> Result<impl Responder, ServiceError> {
match generate_playlist(params.0, params.1.clone()).await { match generate_playlist(&pool.into_inner(), params.0, params.1.clone()).await {
Ok(playlist) => Ok(web::Json(playlist)), Ok(playlist) => Ok(web::Json(playlist)),
Err(e) => Err(e), Err(e) => Err(e),
} }
@ -663,9 +765,10 @@ pub async fn gen_playlist(
#[delete("/playlist/{id}/{date}")] #[delete("/playlist/{id}/{date}")]
#[has_any_role("Role::Admin", "Role::User", type = "Role")] #[has_any_role("Role::Admin", "Role::User", type = "Role")]
pub async fn del_playlist( pub async fn del_playlist(
pool: web::Data<Pool<Sqlite>>,
params: web::Path<(i32, String)>, params: web::Path<(i32, String)>,
) -> Result<impl Responder, ServiceError> { ) -> Result<impl Responder, ServiceError> {
match delete_playlist(params.0, &params.1).await { match delete_playlist(&pool.into_inner(), params.0, &params.1).await {
Ok(_) => Ok(format!("Delete playlist from {} success!", params.1)), Ok(_) => Ok(format!("Delete playlist from {} success!", params.1)),
Err(e) => Err(e), Err(e) => Err(e),
} }
@ -676,16 +779,17 @@ pub async fn del_playlist(
/// **Read Log Life** /// **Read Log Life**
/// ///
/// ```BASH /// ```BASH
/// curl -X Get http://127.0.0.1:8787/api/log/1 /// curl -X GET http://127.0.0.1:8787/api/log/1
/// -H 'Content-Type: application/json' -H 'Authorization: Bearer <TOKEN>' /// -H 'Content-Type: application/json' -H 'Authorization: Bearer <TOKEN>'
/// ``` /// ```
#[get("/log/{id}")] #[get("/log/{id}")]
#[has_any_role("Role::Admin", "Role::User", type = "Role")] #[has_any_role("Role::Admin", "Role::User", type = "Role")]
pub async fn get_log( pub async fn get_log(
pool: web::Data<Pool<Sqlite>>,
id: web::Path<i32>, id: web::Path<i32>,
log: web::Query<DateObj>, log: web::Query<DateObj>,
) -> Result<impl Responder, ServiceError> { ) -> Result<impl Responder, ServiceError> {
read_log_file(&id, &log.date).await read_log_file(&pool.into_inner(), &id, &log.date).await
} }
/// ### File Operations /// ### File Operations
@ -699,10 +803,11 @@ pub async fn get_log(
#[post("/file/{id}/browse/")] #[post("/file/{id}/browse/")]
#[has_any_role("Role::Admin", "Role::User", type = "Role")] #[has_any_role("Role::Admin", "Role::User", type = "Role")]
pub async fn file_browser( pub async fn file_browser(
pool: web::Data<Pool<Sqlite>>,
id: web::Path<i32>, id: web::Path<i32>,
data: web::Json<PathObject>, data: web::Json<PathObject>,
) -> Result<impl Responder, ServiceError> { ) -> Result<impl Responder, ServiceError> {
match browser(*id, &data.into_inner()).await { match browser(&pool.into_inner(), *id, &data.into_inner()).await {
Ok(obj) => Ok(web::Json(obj)), Ok(obj) => Ok(web::Json(obj)),
Err(e) => Err(e), Err(e) => Err(e),
} }
@ -717,10 +822,11 @@ pub async fn file_browser(
#[post("/file/{id}/create-folder/")] #[post("/file/{id}/create-folder/")]
#[has_any_role("Role::Admin", "Role::User", type = "Role")] #[has_any_role("Role::Admin", "Role::User", type = "Role")]
pub async fn add_dir( pub async fn add_dir(
pool: web::Data<Pool<Sqlite>>,
id: web::Path<i32>, id: web::Path<i32>,
data: web::Json<PathObject>, data: web::Json<PathObject>,
) -> Result<HttpResponse, ServiceError> { ) -> Result<HttpResponse, ServiceError> {
create_directory(*id, &data.into_inner()).await create_directory(&pool.into_inner(), *id, &data.into_inner()).await
} }
/// **Rename File** /// **Rename File**
@ -732,10 +838,11 @@ pub async fn add_dir(
#[post("/file/{id}/rename/")] #[post("/file/{id}/rename/")]
#[has_any_role("Role::Admin", "Role::User", type = "Role")] #[has_any_role("Role::Admin", "Role::User", type = "Role")]
pub async fn move_rename( pub async fn move_rename(
pool: web::Data<Pool<Sqlite>>,
id: web::Path<i32>, id: web::Path<i32>,
data: web::Json<MoveObject>, data: web::Json<MoveObject>,
) -> Result<impl Responder, ServiceError> { ) -> Result<impl Responder, ServiceError> {
match rename_file(*id, &data.into_inner()).await { match rename_file(&pool.into_inner(), *id, &data.into_inner()).await {
Ok(obj) => Ok(web::Json(obj)), Ok(obj) => Ok(web::Json(obj)),
Err(e) => Err(e), Err(e) => Err(e),
} }
@ -750,10 +857,11 @@ pub async fn move_rename(
#[post("/file/{id}/remove/")] #[post("/file/{id}/remove/")]
#[has_any_role("Role::Admin", "Role::User", type = "Role")] #[has_any_role("Role::Admin", "Role::User", type = "Role")]
pub async fn remove( pub async fn remove(
pool: web::Data<Pool<Sqlite>>,
id: web::Path<i32>, id: web::Path<i32>,
data: web::Json<PathObject>, data: web::Json<PathObject>,
) -> Result<impl Responder, ServiceError> { ) -> Result<impl Responder, ServiceError> {
match remove_file_or_folder(*id, &data.into_inner().source).await { match remove_file_or_folder(&pool.into_inner(), *id, &data.into_inner().source).await {
Ok(obj) => Ok(web::Json(obj)), Ok(obj) => Ok(web::Json(obj)),
Err(e) => Err(e), Err(e) => Err(e),
} }
@ -762,17 +870,18 @@ pub async fn remove(
/// **Upload File** /// **Upload File**
/// ///
/// ```BASH /// ```BASH
/// curl -X POST http://127.0.0.1:8787/api/file/1/upload/ -H 'Authorization: Bearer <TOKEN>' /// curl -X PUT http://127.0.0.1:8787/api/file/1/upload/ -H 'Authorization: Bearer <TOKEN>'
/// -F "file=@file.mp4" /// -F "file=@file.mp4"
/// ``` /// ```
#[put("/file/{id}/upload/")] #[put("/file/{id}/upload/")]
#[has_any_role("Role::Admin", "Role::User", type = "Role")] #[has_any_role("Role::Admin", "Role::User", type = "Role")]
async fn save_file( async fn save_file(
pool: web::Data<Pool<Sqlite>>,
id: web::Path<i32>, id: web::Path<i32>,
payload: Multipart, payload: Multipart,
obj: web::Query<FileObj>, obj: web::Query<FileObj>,
) -> Result<HttpResponse, ServiceError> { ) -> Result<HttpResponse, ServiceError> {
upload(*id, payload, &obj.path, false).await upload(&pool.into_inner(), *id, payload, &obj.path, false).await
} }
/// **Import playlist** /// **Import playlist**
@ -781,25 +890,126 @@ async fn save_file(
/// lines with leading "#" will be ignore /// lines with leading "#" will be ignore
/// ///
/// ```BASH /// ```BASH
/// curl -X POST http://127.0.0.1:8787/api/file/1/import/ -H 'Authorization: Bearer <TOKEN>' /// curl -X PUT http://127.0.0.1:8787/api/file/1/import/ -H 'Authorization: Bearer <TOKEN>'
/// -F "file=@list.m3u" /// -F "file=@list.m3u"
/// ``` /// ```
#[put("/file/{id}/import/")] #[put("/file/{id}/import/")]
#[has_any_role("Role::Admin", "Role::User", type = "Role")] #[has_any_role("Role::Admin", "Role::User", type = "Role")]
async fn import_playlist( async fn import_playlist(
pool: web::Data<Pool<Sqlite>>,
id: web::Path<i32>, id: web::Path<i32>,
payload: Multipart, payload: Multipart,
obj: web::Query<ImportObj>, obj: web::Query<ImportObj>,
) -> Result<HttpResponse, ServiceError> { ) -> Result<HttpResponse, ServiceError> {
let file = Path::new(&obj.file).file_name().unwrap_or_default(); let file = Path::new(&obj.file).file_name().unwrap_or_default();
let path = env::temp_dir().join(file).to_string_lossy().to_string(); let path = env::temp_dir().join(file).to_string_lossy().to_string();
let (config, _) = playout_config(&id).await?; let (config, _) = playout_config(&pool.clone().into_inner(), &id).await?;
let channel = handles::select_channel(&id).await?; let channel = handles::select_channel(&pool.clone().into_inner(), &id).await?;
upload(*id, payload, &path, true).await?; upload(&pool.into_inner(), *id, payload, &path, true).await?;
import_file(&config, &obj.date, Some(channel.name), &path)?; import_file(&config, &obj.date, Some(channel.name), &path)?;
fs::remove_file(path)?; fs::remove_file(path)?;
Ok(HttpResponse::Ok().into()) Ok(HttpResponse::Ok().into())
} }
/// **Program info**
///
/// Get program infos about given date, or current day
///
/// Examples:
///
/// * get program from current day
/// ```BASH
/// curl -X GET http://127.0.0.1:8787/api/program/1/ -H 'Authorization: Bearer <TOKEN>'
/// ```
///
/// * get a program range between two dates
/// ```BASH
/// curl -X GET http://127.0.0.1:8787/api/program/1/?start_after=2022-11-13T12:00:00&start_before=2022-11-20T11:59:59 \
/// -H 'Authorization: Bearer <TOKEN>'
/// ```
///
/// * get program from give day
/// ```BASH
/// curl -X GET http://127.0.0.1:8787/api/program/1/?start_after=2022-11-13T10:00:00 \
/// -H 'Authorization: Bearer <TOKEN>'
/// ```
#[get("/program/{id}/")]
#[has_any_role("Role::Admin", "Role::User", type = "Role")]
async fn get_program(
pool: web::Data<Pool<Sqlite>>,
id: web::Path<i32>,
obj: web::Query<ProgramObj>,
) -> Result<impl Responder, ServiceError> {
let (config, _) = playout_config(&pool.clone().into_inner(), &id).await?;
let start_sec = config.playlist.start_sec.unwrap();
let mut days = 0;
let mut program = vec![];
let after = obj.start_after;
let mut before = obj.start_before;
if after > before {
before = chrono::Local
.with_ymd_and_hms(after.year(), after.month(), after.day(), 23, 59, 59)
.unwrap()
.naive_local()
}
if start_sec > time_to_sec(&after.format("%H:%M:%S").to_string()) {
days = 1;
}
let date_range = get_date_range(&vec_strings![
(after - Duration::days(days)).format("%Y-%m-%d"),
"-",
before.format("%Y-%m-%d")
]);
for date in date_range {
let conn = pool.clone().into_inner();
let mut naive = NaiveDateTime::parse_from_str(
&format!("{date} {}", sec_to_time(start_sec)),
"%Y-%m-%d %H:%M:%S%.3f",
)
.unwrap();
let playlist = match read_playlist(&conn, *id, date.clone()).await {
Ok(p) => p,
Err(e) => {
error!("Error in Playlist from {date}: {e}");
continue;
}
};
for item in playlist.program {
let start: DateTime<Local> = Local.from_local_datetime(&naive).unwrap();
let source = match Regex::new(&config.text.regex)
.ok()
.and_then(|r| r.captures(&item.source))
{
Some(t) => t[1].to_string(),
None => item.source,
};
let p_item = ProgramItem {
source,
start: start.format("%Y-%m-%d %H:%M:%S%.3f%:z").to_string(),
r#in: item.seek,
out: item.out,
duration: item.duration,
category: item.category,
};
if naive >= after && naive <= before {
program.push(p_item);
}
naive += Duration::milliseconds(((item.out - item.seek) * 1000.0) as i64);
}
}
Ok(web::Json(program))
}

View File

@ -5,7 +5,7 @@ use argon2::{
use rand::{distributions::Alphanumeric, Rng}; use rand::{distributions::Alphanumeric, Rng};
use simplelog::*; use simplelog::*;
use sqlx::{migrate::MigrateDatabase, sqlite::SqliteQueryResult, Pool, Sqlite, SqlitePool}; use sqlx::{migrate::MigrateDatabase, sqlite::SqliteQueryResult, Pool, Sqlite};
use crate::db::models::{Channel, TextPreset, User}; use crate::db::models::{Channel, TextPreset, User};
use crate::utils::{db_path, local_utc_offset, GlobalSettings}; use crate::utils::{db_path, local_utc_offset, GlobalSettings};
@ -15,8 +15,7 @@ struct Role {
name: String, name: String,
} }
async fn create_schema() -> Result<SqliteQueryResult, sqlx::Error> { async fn create_schema(conn: &Pool<Sqlite>) -> Result<SqliteQueryResult, sqlx::Error> {
let conn = connection().await?;
let query = "PRAGMA foreign_keys = ON; let query = "PRAGMA foreign_keys = ON;
CREATE TABLE IF NOT EXISTS global CREATE TABLE IF NOT EXISTS global
( (
@ -71,18 +70,19 @@ async fn create_schema() -> Result<SqliteQueryResult, sqlx::Error> {
FOREIGN KEY (channel_id) REFERENCES channels (id) ON UPDATE SET NULL ON DELETE SET NULL, FOREIGN KEY (channel_id) REFERENCES channels (id) ON UPDATE SET NULL ON DELETE SET NULL,
UNIQUE(mail, username) UNIQUE(mail, username)
);"; );";
let result = sqlx::query(query).execute(&conn).await;
conn.close().await;
result sqlx::query(query).execute(conn).await
} }
pub async fn db_init(domain: Option<String>) -> Result<&'static str, Box<dyn std::error::Error>> { pub async fn db_init(
conn: &Pool<Sqlite>,
domain: Option<String>,
) -> Result<&'static str, Box<dyn std::error::Error>> {
let db_path = db_path()?; let db_path = db_path()?;
if !Sqlite::database_exists(&db_path).await.unwrap_or(false) { if !Sqlite::database_exists(&db_path).await.unwrap_or(false) {
Sqlite::create_database(&db_path).await.unwrap(); Sqlite::create_database(&db_path).await.unwrap();
match create_schema().await { match create_schema(conn).await {
Ok(_) => info!("Database created Successfully"), Ok(_) => info!("Database created Successfully"),
Err(e) => panic!("{e}"), Err(e) => panic!("{e}"),
} }
@ -93,8 +93,6 @@ pub async fn db_init(domain: Option<String>) -> Result<&'static str, Box<dyn std
.map(char::from) .map(char::from)
.collect(); .collect();
let instances = connection().await?;
let url = match domain { let url = match domain {
Some(d) => format!("http://{d}/live/stream.m3u8"), Some(d) => format!("http://{d}/live/stream.m3u8"),
None => "http://localhost/live/stream.m3u8".to_string(), None => "http://localhost/live/stream.m3u8".to_string(),
@ -120,45 +118,30 @@ pub async fn db_init(domain: Option<String>) -> Result<&'static str, Box<dyn std
sqlx::query(query) sqlx::query(query)
.bind(secret) .bind(secret)
.bind(url) .bind(url)
.execute(&instances) .execute(conn)
.await?; .await?;
instances.close().await;
Ok("Database initialized!") Ok("Database initialized!")
} }
pub async fn connection() -> Result<Pool<Sqlite>, sqlx::Error> { pub async fn select_global(conn: &Pool<Sqlite>) -> Result<GlobalSettings, sqlx::Error> {
let db_path = db_path().unwrap();
let conn = SqlitePool::connect(&db_path).await?;
Ok(conn)
}
pub async fn select_global() -> Result<GlobalSettings, sqlx::Error> {
let conn = connection().await?;
let query = "SELECT secret FROM global WHERE id = 1"; let query = "SELECT secret FROM global WHERE id = 1";
let result: GlobalSettings = sqlx::query_as(query).fetch_one(&conn).await?;
conn.close().await;
Ok(result) sqlx::query_as(query).fetch_one(conn).await
} }
pub async fn select_channel(id: &i32) -> Result<Channel, sqlx::Error> { pub async fn select_channel(conn: &Pool<Sqlite>, id: &i32) -> Result<Channel, sqlx::Error> {
let conn = connection().await?;
let query = "SELECT * FROM channels WHERE id = $1"; let query = "SELECT * FROM channels WHERE id = $1";
let mut result: Channel = sqlx::query_as(query).bind(id).fetch_one(&conn).await?; let mut result: Channel = sqlx::query_as(query).bind(id).fetch_one(conn).await?;
conn.close().await;
result.utc_offset = local_utc_offset(); result.utc_offset = local_utc_offset();
Ok(result) Ok(result)
} }
pub async fn select_all_channels() -> Result<Vec<Channel>, sqlx::Error> { pub async fn select_all_channels(conn: &Pool<Sqlite>) -> Result<Vec<Channel>, sqlx::Error> {
let conn = connection().await?;
let query = "SELECT * FROM channels"; let query = "SELECT * FROM channels";
let mut results: Vec<Channel> = sqlx::query_as(query).fetch_all(&conn).await?; let mut results: Vec<Channel> = sqlx::query_as(query).fetch_all(conn).await?;
conn.close().await;
for result in results.iter_mut() { for result in results.iter_mut() {
result.utc_offset = local_utc_offset(); result.utc_offset = local_utc_offset();
@ -167,26 +150,24 @@ pub async fn select_all_channels() -> Result<Vec<Channel>, sqlx::Error> {
Ok(results) Ok(results)
} }
pub async fn update_channel(id: i32, channel: Channel) -> Result<SqliteQueryResult, sqlx::Error> { pub async fn update_channel(
let conn = connection().await?; conn: &Pool<Sqlite>,
id: i32,
channel: Channel,
) -> Result<SqliteQueryResult, sqlx::Error> {
let query = "UPDATE channels SET name = $2, preview_url = $3, config_path = $4, extra_extensions = $5 WHERE id = $1"; let query = "UPDATE channels SET name = $2, preview_url = $3, config_path = $4, extra_extensions = $5 WHERE id = $1";
let result: SqliteQueryResult = sqlx::query(query)
sqlx::query(query)
.bind(id) .bind(id)
.bind(channel.name) .bind(channel.name)
.bind(channel.preview_url) .bind(channel.preview_url)
.bind(channel.config_path) .bind(channel.config_path)
.bind(channel.extra_extensions) .bind(channel.extra_extensions)
.execute(&conn) .execute(conn)
.await?; .await
conn.close().await;
Ok(result)
} }
pub async fn insert_channel(channel: Channel) -> Result<Channel, sqlx::Error> { pub async fn insert_channel(conn: &Pool<Sqlite>, channel: Channel) -> Result<Channel, sqlx::Error> {
let conn = connection().await?;
let query = "INSERT INTO channels (name, preview_url, config_path, extra_extensions, service) VALUES($1, $2, $3, $4, $5)"; let query = "INSERT INTO channels (name, preview_url, config_path, extra_extensions, service) VALUES($1, $2, $3, $4, $5)";
let result = sqlx::query(query) let result = sqlx::query(query)
.bind(channel.name) .bind(channel.name)
@ -194,56 +175,47 @@ pub async fn insert_channel(channel: Channel) -> Result<Channel, sqlx::Error> {
.bind(channel.config_path) .bind(channel.config_path)
.bind(channel.extra_extensions) .bind(channel.extra_extensions)
.bind(channel.service) .bind(channel.service)
.execute(&conn) .execute(conn)
.await?; .await?;
let new_channel: Channel = sqlx::query_as("SELECT * FROM channels WHERE id = $1")
sqlx::query_as("SELECT * FROM channels WHERE id = $1")
.bind(result.last_insert_rowid()) .bind(result.last_insert_rowid())
.fetch_one(&conn) .fetch_one(conn)
.await?; .await
conn.close().await;
Ok(new_channel)
} }
pub async fn delete_channel(id: &i32) -> Result<SqliteQueryResult, sqlx::Error> { pub async fn delete_channel(
let conn = connection().await?; conn: &Pool<Sqlite>,
id: &i32,
) -> Result<SqliteQueryResult, sqlx::Error> {
let query = "DELETE FROM channels WHERE id = $1"; let query = "DELETE FROM channels WHERE id = $1";
let result: SqliteQueryResult = sqlx::query(query).bind(id).execute(&conn).await?;
conn.close().await;
Ok(result) sqlx::query(query).bind(id).execute(conn).await
} }
pub async fn select_role(id: &i32) -> Result<String, sqlx::Error> { pub async fn select_role(conn: &Pool<Sqlite>, id: &i32) -> Result<String, sqlx::Error> {
let conn = connection().await?;
let query = "SELECT name FROM roles WHERE id = $1"; let query = "SELECT name FROM roles WHERE id = $1";
let result: Role = sqlx::query_as(query).bind(id).fetch_one(&conn).await?; let result: Role = sqlx::query_as(query).bind(id).fetch_one(conn).await?;
conn.close().await;
Ok(result.name) Ok(result.name)
} }
pub async fn select_login(user: &str) -> Result<User, sqlx::Error> { pub async fn select_login(conn: &Pool<Sqlite>, user: &str) -> Result<User, sqlx::Error> {
let conn = connection().await?;
let query = "SELECT id, mail, username, password, salt, role_id FROM user WHERE username = $1"; let query = "SELECT id, mail, username, password, salt, role_id FROM user WHERE username = $1";
let result: User = sqlx::query_as(query).bind(user).fetch_one(&conn).await?;
conn.close().await;
Ok(result) sqlx::query_as(query).bind(user).fetch_one(conn).await
} }
pub async fn select_user(user: &str) -> Result<User, sqlx::Error> { pub async fn select_user(conn: &Pool<Sqlite>, user: &str) -> Result<User, sqlx::Error> {
let conn = connection().await?;
let query = "SELECT id, mail, username, role_id FROM user WHERE username = $1"; let query = "SELECT id, mail, username, role_id FROM user WHERE username = $1";
let result: User = sqlx::query_as(query).bind(user).fetch_one(&conn).await?;
conn.close().await;
Ok(result) sqlx::query_as(query).bind(user).fetch_one(conn).await
} }
pub async fn insert_user(user: User) -> Result<SqliteQueryResult, sqlx::Error> { pub async fn insert_user(
let conn = connection().await?; conn: &Pool<Sqlite>,
user: User,
) -> Result<SqliteQueryResult, sqlx::Error> {
let salt = SaltString::generate(&mut OsRng); let salt = SaltString::generate(&mut OsRng);
let password_hash = Argon2::default() let password_hash = Argon2::default()
.hash_password(user.password.clone().as_bytes(), &salt) .hash_password(user.password.clone().as_bytes(), &salt)
@ -251,43 +223,43 @@ pub async fn insert_user(user: User) -> Result<SqliteQueryResult, sqlx::Error> {
let query = let query =
"INSERT INTO user (mail, username, password, salt, role_id) VALUES($1, $2, $3, $4, $5)"; "INSERT INTO user (mail, username, password, salt, role_id) VALUES($1, $2, $3, $4, $5)";
let result = sqlx::query(query)
sqlx::query(query)
.bind(user.mail) .bind(user.mail)
.bind(user.username) .bind(user.username)
.bind(password_hash.to_string()) .bind(password_hash.to_string())
.bind(salt.to_string()) .bind(salt.to_string())
.bind(user.role_id) .bind(user.role_id)
.execute(&conn) .execute(conn)
.await?; .await
conn.close().await;
Ok(result)
} }
pub async fn update_user(id: i32, fields: String) -> Result<SqliteQueryResult, sqlx::Error> { pub async fn update_user(
let conn = connection().await?; conn: &Pool<Sqlite>,
id: i32,
fields: String,
) -> Result<SqliteQueryResult, sqlx::Error> {
let query = format!("UPDATE user SET {fields} WHERE id = $1"); let query = format!("UPDATE user SET {fields} WHERE id = $1");
let result: SqliteQueryResult = sqlx::query(&query).bind(id).execute(&conn).await?;
conn.close().await;
Ok(result) sqlx::query(&query).bind(id).execute(conn).await
} }
pub async fn select_presets(id: i32) -> Result<Vec<TextPreset>, sqlx::Error> { pub async fn select_presets(conn: &Pool<Sqlite>, id: i32) -> Result<Vec<TextPreset>, sqlx::Error> {
let conn = connection().await?;
let query = "SELECT * FROM presets WHERE channel_id = $1"; let query = "SELECT * FROM presets WHERE channel_id = $1";
let result: Vec<TextPreset> = sqlx::query_as(query).bind(id).fetch_all(&conn).await?;
conn.close().await;
Ok(result) sqlx::query_as(query).bind(id).fetch_all(conn).await
} }
pub async fn update_preset(id: &i32, preset: TextPreset) -> Result<SqliteQueryResult, sqlx::Error> { pub async fn update_preset(
let conn = connection().await?; conn: &Pool<Sqlite>,
id: &i32,
preset: TextPreset,
) -> Result<SqliteQueryResult, sqlx::Error> {
let query = let query =
"UPDATE presets SET name = $1, text = $2, x = $3, y = $4, fontsize = $5, line_spacing = $6, "UPDATE presets SET name = $1, text = $2, x = $3, y = $4, fontsize = $5, line_spacing = $6,
fontcolor = $7, alpha = $8, box = $9, boxcolor = $10, boxborderw = 11 WHERE id = $12"; fontcolor = $7, alpha = $8, box = $9, boxcolor = $10, boxborderw = 11 WHERE id = $12";
let result: SqliteQueryResult = sqlx::query(query)
sqlx::query(query)
.bind(preset.name) .bind(preset.name)
.bind(preset.text) .bind(preset.text)
.bind(preset.x) .bind(preset.x)
@ -300,19 +272,19 @@ pub async fn update_preset(id: &i32, preset: TextPreset) -> Result<SqliteQueryRe
.bind(preset.boxcolor) .bind(preset.boxcolor)
.bind(preset.boxborderw) .bind(preset.boxborderw)
.bind(id) .bind(id)
.execute(&conn) .execute(conn)
.await?; .await
conn.close().await;
Ok(result)
} }
pub async fn insert_preset(preset: TextPreset) -> Result<SqliteQueryResult, sqlx::Error> { pub async fn insert_preset(
let conn = connection().await?; conn: &Pool<Sqlite>,
preset: TextPreset,
) -> Result<SqliteQueryResult, sqlx::Error> {
let query = let query =
"INSERT INTO presets (channel_id, name, text, x, y, fontsize, line_spacing, fontcolor, alpha, box, boxcolor, boxborderw) "INSERT INTO presets (channel_id, name, text, x, y, fontsize, line_spacing, fontcolor, alpha, box, boxcolor, boxborderw)
VALUES($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)"; VALUES($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)";
let result: SqliteQueryResult = sqlx::query(query)
sqlx::query(query)
.bind(preset.channel_id) .bind(preset.channel_id)
.bind(preset.name) .bind(preset.name)
.bind(preset.text) .bind(preset.text)
@ -325,18 +297,15 @@ pub async fn insert_preset(preset: TextPreset) -> Result<SqliteQueryResult, sqlx
.bind(preset.r#box) .bind(preset.r#box)
.bind(preset.boxcolor) .bind(preset.boxcolor)
.bind(preset.boxborderw) .bind(preset.boxborderw)
.execute(&conn) .execute(conn)
.await?; .await
conn.close().await;
Ok(result)
} }
pub async fn delete_preset(id: &i32) -> Result<SqliteQueryResult, sqlx::Error> { pub async fn delete_preset(
let conn = connection().await?; conn: &Pool<Sqlite>,
id: &i32,
) -> Result<SqliteQueryResult, sqlx::Error> {
let query = "DELETE FROM presets WHERE id = $1;"; let query = "DELETE FROM presets WHERE id = $1;";
let result: SqliteQueryResult = sqlx::query(query).bind(id).execute(&conn).await?;
conn.close().await;
Ok(result) sqlx::query(query).bind(id).execute(conn).await
} }

View File

@ -1,2 +1,13 @@
use sqlx::{Pool, Sqlite, SqlitePool};
pub mod handles; pub mod handles;
pub mod models; pub mod models;
use crate::utils::db_path;
pub async fn db_pool() -> Result<Pool<Sqlite>, sqlx::Error> {
let db_path = db_path().unwrap();
let conn = SqlitePool::connect(&db_path).await?;
Ok(conn)
}

View File

@ -18,13 +18,13 @@ use api::{
routes::{ routes::{
add_channel, add_dir, add_preset, add_user, control_playout, del_playlist, delete_preset, add_channel, add_dir, add_preset, add_user, control_playout, del_playlist, delete_preset,
file_browser, gen_playlist, get_all_channels, get_channel, get_log, get_playlist, file_browser, gen_playlist, get_all_channels, get_channel, get_log, get_playlist,
get_playout_config, get_presets, get_user, import_playlist, login, media_current, get_playout_config, get_presets, get_program, get_user, import_playlist, login,
media_last, media_next, move_rename, patch_channel, process_control, remove, media_current, media_last, media_next, move_rename, patch_channel, process_control, remove,
remove_channel, save_file, save_playlist, send_text_message, update_playout_config, remove_channel, save_file, save_playlist, send_text_message, update_playout_config,
update_preset, update_user, update_preset, update_user,
}, },
}; };
use db::models::LoginUser; use db::{db_pool, models::LoginUser};
use utils::{args_parse::Args, db_path, init_config, run_args, Role}; use utils::{args_parse::Args, db_path, init_config, run_args, Role};
use ffplayout_lib::utils::{init_logging, PlayoutConfig}; use ffplayout_lib::utils::{init_logging, PlayoutConfig};
@ -64,7 +64,15 @@ async fn main() -> std::io::Result<()> {
let logging = init_logging(&config, None, None); let logging = init_logging(&config, None, None);
CombinedLogger::init(logging).unwrap(); CombinedLogger::init(logging).unwrap();
if let Err(c) = run_args(args.clone()).await { let pool = match db_pool().await {
Ok(p) => p,
Err(e) => {
error!("{e}");
exit(1);
}
};
if let Err(c) = run_args(&pool, args.clone()).await {
exit(c); exit(c);
} }
@ -75,7 +83,7 @@ async fn main() -> std::io::Result<()> {
exit(1); exit(1);
} }
} }
init_config().await; init_config(&pool).await;
let ip_port = conn.split(':').collect::<Vec<&str>>(); let ip_port = conn.split(':').collect::<Vec<&str>>();
let addr = ip_port[0]; let addr = ip_port[0];
let port = ip_port[1].parse::<u16>().unwrap(); let port = ip_port[1].parse::<u16>().unwrap();
@ -85,7 +93,10 @@ async fn main() -> std::io::Result<()> {
// no allow origin here, give it to the reverse proxy // no allow origin here, give it to the reverse proxy
HttpServer::new(move || { HttpServer::new(move || {
let auth = HttpAuthentication::bearer(validator); let auth = HttpAuthentication::bearer(validator);
let db_pool = web::Data::new(pool.clone());
App::new() App::new()
.app_data(db_pool)
.wrap(middleware::Logger::default()) .wrap(middleware::Logger::default())
.service(login) .service(login)
.service( .service(
@ -121,7 +132,8 @@ async fn main() -> std::io::Result<()> {
.service(move_rename) .service(move_rename)
.service(remove) .service(remove)
.service(save_file) .service(save_file)
.service(import_playlist), .service(import_playlist)
.service(get_program),
) )
.service(Files::new("/", public_path()).index_file("index.html")) .service(Files::new("/", public_path()).index_file("index.html"))
}) })

View File

@ -1,12 +1,16 @@
use std::fs; use std::fs;
use simplelog::*; use simplelog::*;
use sqlx::{Pool, Sqlite};
use crate::utils::{control::control_service, errors::ServiceError}; use crate::utils::{control::control_service, errors::ServiceError};
use crate::db::{handles, models::Channel}; use crate::db::{handles, models::Channel};
pub async fn create_channel(target_channel: Channel) -> Result<Channel, ServiceError> { pub async fn create_channel(
conn: &Pool<Sqlite>,
target_channel: Channel,
) -> Result<Channel, ServiceError> {
if !target_channel.service.starts_with("ffplayout@") { if !target_channel.service.starts_with("ffplayout@") {
return Err(ServiceError::BadRequest("Bad service name!".to_string())); return Err(ServiceError::BadRequest("Bad service name!".to_string()));
} }
@ -20,22 +24,22 @@ pub async fn create_channel(target_channel: Channel) -> Result<Channel, ServiceE
&target_channel.config_path, &target_channel.config_path,
)?; )?;
let new_channel = handles::insert_channel(target_channel).await?; let new_channel = handles::insert_channel(conn, target_channel).await?;
control_service(new_channel.id, "enable").await?; control_service(conn, new_channel.id, "enable").await?;
Ok(new_channel) Ok(new_channel)
} }
pub async fn delete_channel(id: i32) -> Result<(), ServiceError> { pub async fn delete_channel(conn: &Pool<Sqlite>, id: i32) -> Result<(), ServiceError> {
let channel = handles::select_channel(&id).await?; let channel = handles::select_channel(conn, &id).await?;
control_service(channel.id, "stop").await?; control_service(conn, channel.id, "stop").await?;
control_service(channel.id, "disable").await?; control_service(conn, channel.id, "disable").await?;
if let Err(e) = fs::remove_file(channel.config_path) { if let Err(e) = fs::remove_file(channel.config_path) {
error!("{e}"); error!("{e}");
}; };
handles::delete_channel(&id).await?; handles::delete_channel(conn, &id).await?;
Ok(()) Ok(())
} }

View File

@ -5,6 +5,7 @@ use reqwest::{
Client, Response, Client, Response,
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sqlx::{Pool, Sqlite};
use crate::db::handles::select_channel; use crate::db::handles::select_channel;
use crate::utils::{errors::ServiceError, playout_config}; use crate::utils::{errors::ServiceError, playout_config};
@ -56,8 +57,8 @@ struct SystemD {
} }
impl SystemD { impl SystemD {
async fn new(id: i32) -> Result<Self, ServiceError> { async fn new(conn: &Pool<Sqlite>, id: i32) -> Result<Self, ServiceError> {
let channel = select_channel(&id).await?; let channel = select_channel(conn, &id).await?;
Ok(Self { Ok(Self {
service: channel.service, service: channel.service,
@ -130,11 +131,15 @@ fn create_header(auth: &str) -> HeaderMap {
headers headers
} }
async fn post_request<T>(id: i32, obj: RpcObj<T>) -> Result<Response, ServiceError> async fn post_request<T>(
conn: &Pool<Sqlite>,
id: i32,
obj: RpcObj<T>,
) -> Result<Response, ServiceError>
where where
T: Serialize, T: Serialize,
{ {
let (config, _) = playout_config(&id).await?; let (config, _) = playout_config(conn, &id).await?;
let url = format!("http://{}", config.rpc_server.address); let url = format!("http://{}", config.rpc_server.address);
let client = Client::new(); let client = Client::new();
@ -151,6 +156,7 @@ where
} }
pub async fn send_message( pub async fn send_message(
conn: &Pool<Sqlite>,
id: i32, id: i32,
message: HashMap<String, String>, message: HashMap<String, String>,
) -> Result<Response, ServiceError> { ) -> Result<Response, ServiceError> {
@ -163,23 +169,35 @@ pub async fn send_message(
}, },
); );
post_request(id, json_obj).await post_request(conn, id, json_obj).await
} }
pub async fn control_state(id: i32, command: String) -> Result<Response, ServiceError> { pub async fn control_state(
conn: &Pool<Sqlite>,
id: i32,
command: String,
) -> Result<Response, ServiceError> {
let json_obj = RpcObj::new(id, "player".into(), ControlParams { control: command }); let json_obj = RpcObj::new(id, "player".into(), ControlParams { control: command });
post_request(id, json_obj).await post_request(conn, id, json_obj).await
} }
pub async fn media_info(id: i32, command: String) -> Result<Response, ServiceError> { pub async fn media_info(
conn: &Pool<Sqlite>,
id: i32,
command: String,
) -> Result<Response, ServiceError> {
let json_obj = RpcObj::new(id, "player".into(), MediaParams { media: command }); let json_obj = RpcObj::new(id, "player".into(), MediaParams { media: command });
post_request(id, json_obj).await post_request(conn, id, json_obj).await
} }
pub async fn control_service(id: i32, command: &str) -> Result<String, ServiceError> { pub async fn control_service(
let system_d = SystemD::new(id).await?; conn: &Pool<Sqlite>,
id: i32,
command: &str,
) -> Result<String, ServiceError> {
let system_d = SystemD::new(conn, id).await?;
match command { match command {
"enable" => system_d.enable(), "enable" => system_d.enable(),

View File

@ -6,6 +6,7 @@ use futures_util::TryStreamExt as _;
use rand::{distributions::Alphanumeric, Rng}; use rand::{distributions::Alphanumeric, Rng};
use relative_path::RelativePath; use relative_path::RelativePath;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sqlx::{Pool, Sqlite};
use simplelog::*; use simplelog::*;
@ -87,8 +88,12 @@ fn norm_abs_path(root_path: &str, input_path: &str) -> (PathBuf, String, String)
/// Take input path and give file and folder list from it back. /// Take input path and give file and folder list from it back.
/// Input should be a relative path segment, but when it is a absolut path, the norm_abs_path function /// Input should be a relative path segment, but when it is a absolut path, the norm_abs_path function
/// will take care, that user can not break out from given storage path in config. /// will take care, that user can not break out from given storage path in config.
pub async fn browser(id: i32, path_obj: &PathObject) -> Result<PathObject, ServiceError> { pub async fn browser(
let (config, _) = playout_config(&id).await?; conn: &Pool<Sqlite>,
id: i32,
path_obj: &PathObject,
) -> Result<PathObject, ServiceError> {
let (config, _) = playout_config(conn, &id).await?;
let extensions = config.storage.extensions; let extensions = config.storage.extensions;
let (path, parent, path_component) = norm_abs_path(&config.storage.path, &path_obj.source); let (path, parent, path_component) = norm_abs_path(&config.storage.path, &path_obj.source);
let mut obj = PathObject::new(path_component, Some(parent)); let mut obj = PathObject::new(path_component, Some(parent));
@ -143,10 +148,11 @@ pub async fn browser(id: i32, path_obj: &PathObject) -> Result<PathObject, Servi
} }
pub async fn create_directory( pub async fn create_directory(
conn: &Pool<Sqlite>,
id: i32, id: i32,
path_obj: &PathObject, path_obj: &PathObject,
) -> Result<HttpResponse, ServiceError> { ) -> Result<HttpResponse, ServiceError> {
let (config, _) = playout_config(&id).await?; let (config, _) = playout_config(conn, &id).await?;
let (path, _, _) = norm_abs_path(&config.storage.path, &path_obj.source); let (path, _, _) = norm_abs_path(&config.storage.path, &path_obj.source);
if let Err(e) = fs::create_dir_all(&path) { if let Err(e) = fs::create_dir_all(&path) {
@ -198,8 +204,12 @@ fn rename(source: &PathBuf, target: &PathBuf) -> Result<MoveObject, ServiceError
} }
} }
pub async fn rename_file(id: i32, move_object: &MoveObject) -> Result<MoveObject, ServiceError> { pub async fn rename_file(
let (config, _) = playout_config(&id).await?; conn: &Pool<Sqlite>,
id: i32,
move_object: &MoveObject,
) -> Result<MoveObject, ServiceError> {
let (config, _) = playout_config(conn, &id).await?;
let (source_path, _, _) = norm_abs_path(&config.storage.path, &move_object.source); let (source_path, _, _) = norm_abs_path(&config.storage.path, &move_object.source);
let (mut target_path, _, _) = norm_abs_path(&config.storage.path, &move_object.target); let (mut target_path, _, _) = norm_abs_path(&config.storage.path, &move_object.target);
@ -229,8 +239,12 @@ pub async fn rename_file(id: i32, move_object: &MoveObject) -> Result<MoveObject
Err(ServiceError::InternalServerError) Err(ServiceError::InternalServerError)
} }
pub async fn remove_file_or_folder(id: i32, source_path: &str) -> Result<(), ServiceError> { pub async fn remove_file_or_folder(
let (config, _) = playout_config(&id).await?; conn: &Pool<Sqlite>,
id: i32,
source_path: &str,
) -> Result<(), ServiceError> {
let (config, _) = playout_config(conn, &id).await?;
let (source, _, _) = norm_abs_path(&config.storage.path, source_path); let (source, _, _) = norm_abs_path(&config.storage.path, source_path);
if !source.exists() { if !source.exists() {
@ -262,8 +276,8 @@ pub async fn remove_file_or_folder(id: i32, source_path: &str) -> Result<(), Ser
Err(ServiceError::InternalServerError) Err(ServiceError::InternalServerError)
} }
async fn valid_path(id: i32, path: &str) -> Result<PathBuf, ServiceError> { async fn valid_path(conn: &Pool<Sqlite>, id: i32, path: &str) -> Result<PathBuf, ServiceError> {
let (config, _) = playout_config(&id).await?; let (config, _) = playout_config(conn, &id).await?;
let (test_path, _, _) = norm_abs_path(&config.storage.path, path); let (test_path, _, _) = norm_abs_path(&config.storage.path, path);
if !test_path.is_dir() { if !test_path.is_dir() {
@ -274,6 +288,7 @@ async fn valid_path(id: i32, path: &str) -> Result<PathBuf, ServiceError> {
} }
pub async fn upload( pub async fn upload(
conn: &Pool<Sqlite>,
id: i32, id: i32,
mut payload: Multipart, mut payload: Multipart,
path: &str, path: &str,
@ -296,7 +311,7 @@ pub async fn upload(
if abs_path { if abs_path {
filepath = PathBuf::from(path); filepath = PathBuf::from(path);
} else { } else {
let target_path = valid_path(id, path).await?; let target_path = valid_path(conn, id, path).await?;
filepath = target_path.join(filename); filepath = target_path.join(filename);
} }

View File

@ -5,11 +5,13 @@ use std::{
path::Path, path::Path,
}; };
use chrono::prelude::*; use chrono::{format::ParseErrorKind, prelude::*};
use faccess::PathExt; use faccess::PathExt;
use once_cell::sync::OnceCell; use once_cell::sync::OnceCell;
use rpassword::read_password; use rpassword::read_password;
use serde::{de, Deserialize, Deserializer};
use simplelog::*; use simplelog::*;
use sqlx::{Pool, Sqlite};
pub mod args_parse; pub mod args_parse;
pub mod channels; pub mod channels;
@ -23,7 +25,7 @@ use crate::db::{
models::{Channel, User}, models::{Channel, User},
}; };
use crate::utils::{args_parse::Args, errors::ServiceError}; use crate::utils::{args_parse::Args, errors::ServiceError};
use ffplayout_lib::utils::PlayoutConfig; use ffplayout_lib::utils::{time_to_sec, PlayoutConfig};
#[derive(Clone, Eq, PartialEq)] #[derive(Clone, Eq, PartialEq)]
pub enum Role { pub enum Role {
@ -48,8 +50,8 @@ pub struct GlobalSettings {
} }
impl GlobalSettings { impl GlobalSettings {
async fn new() -> Self { async fn new(conn: &Pool<Sqlite>) -> Self {
let global_settings = select_global(); let global_settings = select_global(conn);
match global_settings.await { match global_settings.await {
Ok(g) => g, Ok(g) => g,
@ -66,8 +68,8 @@ impl GlobalSettings {
static INSTANCE: OnceCell<GlobalSettings> = OnceCell::new(); static INSTANCE: OnceCell<GlobalSettings> = OnceCell::new();
pub async fn init_config() { pub async fn init_config(conn: &Pool<Sqlite>) {
let config = GlobalSettings::new().await; let config = GlobalSettings::new(conn).await;
INSTANCE.set(config).unwrap(); INSTANCE.set(config).unwrap();
} }
@ -88,7 +90,7 @@ pub fn db_path() -> Result<String, Box<dyn std::error::Error>> {
Ok(db_path) Ok(db_path)
} }
pub async fn run_args(mut args: Args) -> Result<(), i32> { pub async fn run_args(conn: &Pool<Sqlite>, mut args: Args) -> Result<(), i32> {
if !args.init && args.listen.is_none() && !args.ask && args.username.is_none() { if !args.init && args.listen.is_none() && !args.ask && args.username.is_none() {
error!("Wrong number of arguments! Run ffpapi --help for more information."); error!("Wrong number of arguments! Run ffpapi --help for more information.");
@ -96,7 +98,7 @@ pub async fn run_args(mut args: Args) -> Result<(), i32> {
} }
if args.init { if args.init {
if let Err(e) = db_init(args.domain).await { if let Err(e) = db_init(conn, args.domain).await {
panic!("{e}"); panic!("{e}");
}; };
@ -160,7 +162,7 @@ pub async fn run_args(mut args: Args) -> Result<(), i32> {
token: None, token: None,
}; };
if let Err(e) = insert_user(user).await { if let Err(e) = insert_user(conn, user).await {
error!("{e}"); error!("{e}");
return Err(1); return Err(1);
}; };
@ -175,13 +177,18 @@ pub async fn run_args(mut args: Args) -> Result<(), i32> {
pub fn read_playout_config(path: &str) -> Result<PlayoutConfig, Box<dyn Error>> { pub fn read_playout_config(path: &str) -> Result<PlayoutConfig, Box<dyn Error>> {
let file = File::open(path)?; let file = File::open(path)?;
let config: PlayoutConfig = serde_yaml::from_reader(file)?; let mut config: PlayoutConfig = serde_yaml::from_reader(file)?;
config.playlist.start_sec = Some(time_to_sec(&config.playlist.day_start));
Ok(config) Ok(config)
} }
pub async fn playout_config(channel_id: &i32) -> Result<(PlayoutConfig, Channel), ServiceError> { pub async fn playout_config(
if let Ok(channel) = select_channel(channel_id).await { conn: &Pool<Sqlite>,
channel_id: &i32,
) -> Result<(PlayoutConfig, Channel), ServiceError> {
if let Ok(channel) = select_channel(conn, channel_id).await {
if let Ok(config) = read_playout_config(&channel.config_path.clone()) { if let Ok(config) = read_playout_config(&channel.config_path.clone()) {
return Ok((config, channel)); return Ok((config, channel));
} }
@ -192,8 +199,12 @@ pub async fn playout_config(channel_id: &i32) -> Result<(PlayoutConfig, Channel)
)) ))
} }
pub async fn read_log_file(channel_id: &i32, date: &str) -> Result<String, ServiceError> { pub async fn read_log_file(
if let Ok(channel) = select_channel(channel_id).await { conn: &Pool<Sqlite>,
channel_id: &i32,
date: &str,
) -> Result<String, ServiceError> {
if let Ok(channel) = select_channel(conn, channel_id).await {
let mut date_str = "".to_string(); let mut date_str = "".to_string();
if !date.is_empty() { if !date.is_empty() {
@ -234,3 +245,22 @@ pub fn local_utc_offset() -> i32 {
utc_offset utc_offset
} }
pub fn naive_date_time_from_str<'de, D>(deserializer: D) -> Result<NaiveDateTime, D::Error>
where
D: Deserializer<'de>,
{
let s: String = Deserialize::deserialize(deserializer)?;
match NaiveDateTime::parse_from_str(&s, "%Y-%m-%dT%H:%M:%S") {
Ok(date_time) => Ok(date_time),
Err(e) => {
if e.kind() == ParseErrorKind::TooShort {
NaiveDateTime::parse_from_str(&format!("{s}T00:00:00"), "%Y-%m-%dT%H:%M:%S")
.map_err(de::Error::custom)
} else {
NaiveDateTime::parse_from_str(&s, "%Y-%m-%dT%H:%M:%S%#z").map_err(de::Error::custom)
}
}
}
}

View File

@ -1,14 +1,19 @@
use std::{fs, path::PathBuf}; use std::{fs, path::PathBuf};
use simplelog::*; use simplelog::*;
use sqlx::{Pool, Sqlite};
use crate::utils::{errors::ServiceError, playout_config}; use crate::utils::{errors::ServiceError, playout_config};
use ffplayout_lib::utils::{ use ffplayout_lib::utils::{
generate_playlist as playlist_generator, json_reader, json_writer, JsonPlaylist, generate_playlist as playlist_generator, json_reader, json_writer, JsonPlaylist,
}; };
pub async fn read_playlist(id: i32, date: String) -> Result<JsonPlaylist, ServiceError> { pub async fn read_playlist(
let (config, _) = playout_config(&id).await?; conn: &Pool<Sqlite>,
id: i32,
date: String,
) -> Result<JsonPlaylist, ServiceError> {
let (config, _) = playout_config(conn, &id).await?;
let mut playlist_path = PathBuf::from(&config.playlist.path); let mut playlist_path = PathBuf::from(&config.playlist.path);
let d: Vec<&str> = date.split('-').collect(); let d: Vec<&str> = date.split('-').collect();
playlist_path = playlist_path playlist_path = playlist_path
@ -23,8 +28,12 @@ pub async fn read_playlist(id: i32, date: String) -> Result<JsonPlaylist, Servic
} }
} }
pub async fn write_playlist(id: i32, json_data: JsonPlaylist) -> Result<String, ServiceError> { pub async fn write_playlist(
let (config, _) = playout_config(&id).await?; conn: &Pool<Sqlite>,
id: i32,
json_data: JsonPlaylist,
) -> Result<String, ServiceError> {
let (config, _) = playout_config(conn, &id).await?;
let date = json_data.date.clone(); let date = json_data.date.clone();
let mut playlist_path = PathBuf::from(&config.playlist.path); let mut playlist_path = PathBuf::from(&config.playlist.path);
let d: Vec<&str> = date.split('-').collect(); let d: Vec<&str> = date.split('-').collect();
@ -68,8 +77,12 @@ pub async fn write_playlist(id: i32, json_data: JsonPlaylist) -> Result<String,
Err(ServiceError::InternalServerError) Err(ServiceError::InternalServerError)
} }
pub async fn generate_playlist(id: i32, date: String) -> Result<JsonPlaylist, ServiceError> { pub async fn generate_playlist(
let (mut config, channel) = playout_config(&id).await?; conn: &Pool<Sqlite>,
id: i32,
date: String,
) -> Result<JsonPlaylist, ServiceError> {
let (mut config, channel) = playout_config(conn, &id).await?;
config.general.generate = Some(vec![date.clone()]); config.general.generate = Some(vec![date.clone()]);
match playlist_generator(&config, Some(channel.name)) { match playlist_generator(&config, Some(channel.name)) {
@ -89,8 +102,8 @@ pub async fn generate_playlist(id: i32, date: String) -> Result<JsonPlaylist, Se
} }
} }
pub async fn delete_playlist(id: i32, date: &str) -> Result<(), ServiceError> { pub async fn delete_playlist(conn: &Pool<Sqlite>, id: i32, date: &str) -> Result<(), ServiceError> {
let (config, _) = playout_config(&id).await?; let (config, _) = playout_config(conn, &id).await?;
let mut playlist_path = PathBuf::from(&config.playlist.path); let mut playlist_path = PathBuf::from(&config.playlist.path);
let d: Vec<&str> = date.split('-').collect(); let d: Vec<&str> = date.split('-').collect();
playlist_path = playlist_path playlist_path = playlist_path

View File

@ -4,7 +4,7 @@ description = "24/7 playout based on rust and ffmpeg"
license = "GPL-3.0" license = "GPL-3.0"
authors = ["Jonathan Baecker jonbae77@gmail.com"] authors = ["Jonathan Baecker jonbae77@gmail.com"]
readme = "README.md" readme = "README.md"
version = "0.16.3" version = "0.16.4"
edition = "2021" edition = "2021"
default-run = "ffplayout" default-run = "ffplayout"

@ -1 +1 @@
Subproject commit 535624d81bc58721314e7c392eafe8614b0384f8 Subproject commit 269d3f0306a230805f7a19477163631c07bbd0a3

View File

@ -4,7 +4,7 @@ description = "Library for ffplayout"
license = "GPL-3.0" license = "GPL-3.0"
authors = ["Jonathan Baecker jonbae77@gmail.com"] authors = ["Jonathan Baecker jonbae77@gmail.com"]
readme = "README.md" readme = "README.md"
version = "0.16.3" version = "0.16.4"
edition = "2021" edition = "2021"
[dependencies] [dependencies]

View File

@ -14,41 +14,12 @@ use std::{
sync::{atomic::AtomicUsize, Arc, Mutex}, sync::{atomic::AtomicUsize, Arc, Mutex},
}; };
use chrono::{Duration, NaiveDate};
use simplelog::*; use simplelog::*;
use super::folder::FolderSource; use super::folder::FolderSource;
use crate::utils::{json_serializer::JsonPlaylist, time_to_sec, Media, PlayoutConfig}; use crate::utils::{
get_date_range, json_serializer::JsonPlaylist, time_to_sec, Media, PlayoutConfig,
/// Generate a vector with dates, from given range. };
fn get_date_range(date_range: &[String]) -> Vec<String> {
let mut range = vec![];
let start = match NaiveDate::parse_from_str(&date_range[0], "%Y-%m-%d") {
Ok(s) => s,
Err(_) => {
error!("date format error in: <yellow>{:?}</>", date_range[0]);
exit(1);
}
};
let end = match NaiveDate::parse_from_str(&date_range[2], "%Y-%m-%d") {
Ok(e) => e,
Err(_) => {
error!("date format error in: <yellow>{:?}</>", date_range[2]);
exit(1);
}
};
let duration = end.signed_duration_since(start);
let days = duration.num_days() + 1;
for day in 0..days {
range.push((start + Duration::days(day)).format("%Y-%m-%d").to_string());
}
range
}
/// Generate playlists /// Generate playlists
pub fn generate_playlist( pub fn generate_playlist(

View File

@ -797,6 +797,36 @@ pub fn test_tcp_port(url: &str) -> bool {
false false
} }
/// Generate a vector with dates, from given range.
pub fn get_date_range(date_range: &[String]) -> Vec<String> {
let mut range = vec![];
let start = match NaiveDate::parse_from_str(&date_range[0], "%Y-%m-%d") {
Ok(s) => s,
Err(_) => {
error!("date format error in: <yellow>{:?}</>", date_range[0]);
exit(1);
}
};
let end = match NaiveDate::parse_from_str(&date_range[2], "%Y-%m-%d") {
Ok(e) => e,
Err(_) => {
error!("date format error in: <yellow>{:?}</>", date_range[2]);
exit(1);
}
};
let duration = end.signed_duration_since(start);
let days = duration.num_days() + 1;
for day in 0..days {
range.push((start + Duration::days(day)).format("%Y-%m-%d").to_string());
}
range
}
pub fn home_dir() -> Option<PathBuf> { pub fn home_dir() -> Option<PathBuf> {
home_dir_inner() home_dir_inner()
} }