Merge branch 'develop'

This commit is contained in:
Justin Emter
2017-09-04 17:19:53 -07:00
19 changed files with 1096 additions and 1060 deletions

3
.gitignore vendored
View File

@@ -11,3 +11,6 @@ pseudo-channel.db
env/
*.log
*.pid
*.json
.cache/
.prevplaying

File diff suppressed because it is too large Load Diff

View File

@@ -1,12 +1,11 @@
# PseudoChannel.py - a Plex Controller for Home-Brewed TV Channels
# PseudoChannel.py - Your Home-Brewed TV Channels
Joined by the author of [Fake TV](https://medium.com/@Fake.TV), this project aims at tackling one issue: creating a fake tv channel experience with your own media library (movies, tv shows, commercials, etc.). The idea is super simple... when you turn on your TV, rather than hopping straight to Netflix, you can choose to watch your own channel of curated media like a real channel, with randomized movie time blocks, weekend morning cartoons, 90's commercials to fill up gaps and more. We aim to add a ton of neat features but the basic idea is to have something that feels like a real TV channel. That being said it isn't supposed to "pause" nor are you supposed to intervene too much. Just like a real channel you are presented with a channel that you define once and let it go as it advances in series episodes, playing random movies where specified (defined by various parameters like genre, "Kevin Bacon", etc.). Think: weekday movie nights @ 8:00 PM. Or perhaps you want to further specify your weekly Wednesday evening movie be a movie in your Plex library that stars "Will Smith". PseudoChannel is built to interface with the Plex media server. So if you want to have your very own PseudoChannel, you first need to set up your home server using [Plex](https://www.plex.tv/). After that you can come back here to learn how to setup everything else. Although this app runs using Python and the command line, we aim to make all of it as easy as possible to understand for those who are intimidated by this sort of technology. If you have a question, are troubleshooting or have feature ideas, just leave an 'issue' here in this repository.
Joined by the author of [Fake TV](https://medium.com/@Fake.TV), this project aims at tackling one issue: creating a fake tv channel experience with your own media library (movies, tv shows, commercials, etc.). The idea is super simple... when you turn on your TV, rather than hopping straight to Netflix, you can choose to watch your own channel of curated media like a real channel, with randomized movie time blocks, weekend morning cartoons, 90's commercials to fill up gaps and more. We aim to add a ton of neat features but the basic idea is to have something that feels like a real TV channel. That being said it isn't supposed to "pause" nor are you supposed to intervene too much. Just like a real channel you are presented with a channel that you define once and let it go as it advances in series episodes, playing random movies where specified (defined by various parameters like genre, "Kevin Bacon", etc.). Think: weekday movie nights @ 8:00 PM. Or perhaps you want to further specify your weekly Wednesday evening movie be a movie in your Plex library that stars "Will Smith". Currently the latter feature among many others are being developed but this is the goal. PseudoChannel is built to interface with the Plex media server. So if you want to have your very own PseudoChannel, you first need to set up your home server using [Plex](https://www.plex.tv/). After that you can come back here to learn how to setup everything else. Please note that we just started this project so everything is evolving rapidly. Check back often. We aim to have a decent working "alpha" version within a week or so. This readme / the how-to guide will all be very user friendly. Although this app runs using Python and the command line, we aim to make all of it as easy as possible to understand for those who are intimidated by this sort of technology.
![Generated HTML schedule](http://i.imgur.com/uTGRYIp.png)
If interested in this project, check back very soon when the alpha is up. It's close and a tiny bit more user friendly. :)
## How to Use (in the case someone stumbles across this and wants to try it before its polished):
## How to Use:
- The instructions below are all for configuring the **"controller"** device (i.e. a laptop or raspberry pi running linux). This is the device this app runs on to control the Plex client. The **"client"** device should be a Raspberry Pi running Rasplex hooked up to your TV via HDMI - although I'm sure other devices work great too (never tried).
@@ -25,21 +24,22 @@ baseurl = 'http://192.168.1.28:32400'
```
*This file is important as it tells PseudoChannel.py how/where to connect to your Plex server. It should sit just outside of this /pseudo-channel/ directory.*
3. Edit the `pseudo_config.py` / `the pseudo_schedule.xml` to your liking. You can specify your plex media library names within the `pseudo_config.py` file... the default assumes that you have these libraries in your Plex server named like so: "TV Shows", "Movies" & "Commercials". If you do not intend on using commercials just set the `useCommercialInjection` flag to `False`. There are a few other experimental options like using Google Calendar rather than an XML. It is an arduous process to initially set up and I've found the XML method to be the easiest method for organizing your schedule - so stick with that for now.
3. Edit the `pseudo_config.py` / `the pseudo_schedule.xml` to your liking. You can specify your plex media library names within the `pseudo_config.py` file... If you do not intend on using commercials just set the `useCommercialInjection` flag to `False`. There are a few other experimental options like using Google Calendar rather than an XML. It is an arduous process to initially set up and I've found the XML method to be the easiest method for organizing your schedule - so stick with that for now. Finally, setup your schedule in the xml file. There are some detailed instructions commented at the top of that file.
4. Run the `PseudoChannel.py` file with the following flags:
```bash
% python PseudoChannel.py -u -xml -g -r
% python PseudoChannel.py -u -xml -g -m -r
```
*You can also run `-h` to view all the options. Keep in mind not all options are operational & some are experimental. Stick with the ones above and use `-c` to find the name(s) of your Plex client(s).*
*You can also run `-h` to view all the options. Use `-c` to find the name(s) of your Plex client(s) to add to the config.*
- The `-u` flag will prepare & update (& create if not exists) the local `pseudo-channel.db`, you only need to run this once in the beginning or later when you have added new media to your Plex libraries.
- The `-xml` flag will update the newly created local db with your schedule from the xml file - you should run this everytime you make changes to the xml.
- The `-g` file will generate the daily schedule (for today) based on the xml. This is useful for the first run or testing (or manually advancing the daily queue forward). Running this flag say, 15 times will advance the play queue forward 15 days. It is automatically run every night at midnight to generate the daily schedule.
- Finally, the `-r` flag will run the app, checking the time / triggering the playstate of any media that is scheduled. It will also update the daily schedule when the clock hits 11.59 (or whatever time you've configured in the config file). The xml schedule is a bit tempermental at the moment so if you see errors, check your entries there first. Make sure all of your movie names / TV Series names are correct.
- The `-m` flag makes both the .html/.xml files and starts a simple html web server in the `./schedules` directory.
- Finally, the `-r` flag will run the app, checking the time / triggering the playstate of any media that is scheduled. It will also update the daily schedule when the clock hits 11.59 (or whatever time you've configured in the config file). If you see errors, check your entries in the xml first. Check your times, check for overlaps & make sure your are using ascii characters to replace foreign characters like umlauts and '&' characters, etc. Make sure all of your movie names / TV Series names are correct.
You can run `% python PseudoChannel.py` with the following options. The order is important (i.e. `% python PseudoChannel.py -u -xml -g -m -r`):
You can run `% python PseudoChannel.py` with the following options. The order is important depending on what you are doing (i.e. `% python PseudoChannel.py -u -xml -g -m -r`):
| Flag | Description |
| ------------------------|--------------|
@@ -50,16 +50,22 @@ You can run `% python PseudoChannel.py` with the following options. The order is
| -c, --show_clients | Show connected Plex clients. |
| -s, --show_schedule | Output the generated "Daily Schedule" to your terminal. |
| -m, --make_html | Manually generate both html / xml docs based on the "Daily Schedule". |
| -e, --export | Export the current queue of your "TV Shows" episodes. Useful when redoing your local DB. |
| -i, --import | Import the previously exported queue of your "TV Shows" episodes. |
## `startstop.sh` - Alternative Way of Running the Application:
Within the app directory, there is a file named, `startstop.sh`. This bash script is a convenient way to start/stop the application. When run, it will start the application and save the "pid" of the process to the application directory. When run again, it will check to see if that process is running, if so it will stop it. All you have to do is run:
```bash
% ./startstop.sh
```
When you start the application with this bash script, you can close your terminal as it will keep running in the background. Later, when you come back and want to stop it... you can just execute that file once more and it will stop the running process. Please note: It's good to test the application and your configurations using the manual process above before running this bash script. Although there is a `pseudo-channel.log` that is created within the application directory, it is easier to just view the output in your terminal window - something that won't happen when using the bash script.
## Futher Info:
Features are being added to the xml but as of now there are a few. Within the XML `<time>` entry you are able to pass in various attributes to set certain values. As of now, aside from "title" and "type" which are mandatory, you can take advantage of "time-shift". This parameter accepts values in minutes and can be no lower than "1". If the attribute, "strict-time" is set to "false", then this `<time>` entry will be shifted to a new time based on the previous time with a smaller gap calculated according to the value in "time-shift". Basically, if you do not want any gaps in your daily generated schedule you would leave "strict-time" false and set "time-shift" to "1" for all `<time>` entries. However, this will create a schedule with weird start times like, "1:57 PM". Taking advantage of the "time-shift" perameter will correct this. If you set it to a value of "5", all media is shifted and hooked on to a "pretty" time that is a multiple of 5. So if used, rather then having a "Seinfeld" episode being set to "1:57 PM" it may be recalculated and scheduled for "2:00 PM". However, if you would like to make sure that "The Simpsons" will always start every weekday at "6:00 PM" then you can simply set that `<time>` entry to `srtict-time="true"`. This will ensure that despite other non-strict times shifting around, "The Simpsons" will air every weekday at the desired "6:00 PM" as scheduled (be sure that you haven't accidentally made two time entries "strict-time" for the same day/time - this sort of thing will cause weird scheduling errors). When using "strict-time" or having the "time-shift" value > than 1 (minute), this will result in empty gaps in the schedule. Currently I have a flag in the config for "commercial injection" to fill up the gaps as much as possible with commercials from commercials in your Plex server "Commercials" library. If you do not want to use this feature or if you don't have any commercials in your Plex server, just open up that `pseudo_config.py` file and set `useCommercialInjection` to `False`.
## Power Saving / Using an External Controller:
*Warning: this section is useful only when getting the app working well manually. I highly recommend sticking with the manual methods of running PseudoChannel.py before attempting these features. It also helps to fully understand how the app works before implementing an external controller. You can better understand this app by tinkering with the methods above before attempting this part. When in doubt, just reach out to me here by creating an issue in this repo.*
If you'd like to use your PseudoChannel but do not want your media playing when you're not watching it, you have a handful of options to make the channel even cooler. When you run the app using `python PseudoChannel.py -r` it checks the `daily_schedule` that was generated for the day to see where the playhead is when you execute the app... if it happens to be `1:17 PM` when you run the app and "Seinfeld" started at `1:10 PM`, then the app will start playing the scheduled "Seinfeld" episode exactly 7 minutes in as it is scheduled for the day (down to the second). This is nice but what if you want to start / stop the app dynamically using a button hooked up to your Raspberry Pi? Or perhaps you'd like to setup a voice command using your Amazon Echo. All you have to do is trigger the `startstop.sh` bash file. So let's say you decided to use an IR Led reciever to catch commands sent by your TV remote. After you configure the IR circuit on your Raspberry Pi, you would set it to execute the `startstop.sh` file - this will then either start PseudoChannel.py & resume the daily schedule if it isn't already running, or stop the currently running PseudoChannel.py instance if it is running. Doing this will pick up the playhead exactly where it would be as if the app never stopped. However, if you decided to use PseudoChannel in this way you need to manually setup a crontab to trigger the daily update every night at `12:00 AM`. Since the app isn't running unless you trigger it, it needs to know to update the daily schedule and advance forward in the queue. To do this, there is another bash file named, `generate_daily_sched.sh`. This is the file that should be setup with crontab to be triggered every evening at midnight. Essentially, this file will check to see if the app has been started via, `startstop.sh` - if so it will do nothing as the running app will know to update when the clock hits midnight; if however, it finds that the `startstop.sh` script did not spawn an instance of the app, it will trigger, `python PseudoChannel.py -g` which will generate the daily schedule. It's important to note that you should only use the two bash scripts, `startstop.sh` & `generate_daily_sched.sh` with each other. If you have previously started the app using the command, `python PseudoChannel.py -r`, you should kill it before using the, `startstop.sh` or the `generate_daily_sched.sh`. So basically, if you decide to use this neat power-saving / dynamic controlling feature using the bash scripts, then stick with that. If you accidentally have an instance of the app running using `PseudoChannel.py` with the `-r` flag and also have the bash file setup with a crontab, you will unwittingly trigger the app to generate the daily schedule twice at the same time when the clock hits midnight. Before attempting any of this I recommend keeping it simple and just manually running the app. This will also help you easily view debug info incase you run into XML issues or other unforseen errs. Once everything seems to be running great, then move to this method.
Features are being added to the xml but as of now there are a few. Within the XML `<time>` entry you are able to pass in various attributes to set certain values. As of now, aside from "title" and "type" which are mandatory, you can take advantage of "time-shift". This parameter accepts values in minutes and can be no lower than "1". If the attribute, "strict-time" is set to "false", then this `<time>` entry will be shifted to a new time based on the previous time with a smaller gap calculated according to the value in "time-shift". Basically, if you do not want any gaps in your daily generated schedule you would leave "strict-time" false and set "time-shift" to "1" for all `<time>` entries. However, this will create a schedule with weird start times like, "1:57 PM". Taking advantage of the "time-shift" perameter will correct this. If you set it to a value of "5", all media is shifted and hooked on to a "pretty" time that is a multiple of 5. So if used, rather then having a "Seinfeld" episode being set to "1:57 PM" it may be recalculated and scheduled for "2:00 PM". However, if you would like to make sure that "The Simpsons" will always start every weekday at "6:00 PM" then you can simply set that `<time>` entry to `srtict-time="true"`. This will ensure that despite other non-strict times shifting around, "The Simpsons" will air every weekday at the desired "6:00 PM" as scheduled (be sure that you haven't accidentally made two time entries "strict-time" for the same day/time - this sort of thing will cause weird scheduling errors). When using "strict-time" or having the "time-shift" value > than 1 (minute), this will result in empty gaps in the schedule. Currently I have a flag in the config for "commercial injection" to fill up the gaps as much as possible with commercials from commercials in your Plex server "Commercials" library. If you do not want to use this feature or if you don't have any commercials in your Plex server, just open up that `pseudo_config.py` file and set `useCommercialInjection` to `False`. Please check the `pseudo-channel.xml` for more information.
## The Automatically Generated .HTML Daily Schedule / Server
@@ -74,6 +80,4 @@ http://192.168.1.28:8000
Stay tuned for a polished version / bug fixes. I've also started a user friendly web version that hopefully will be working soon.
Special thanks to Mark @ [Fake TV](https://medium.com/@Fake.TV). Without his creative ideas and love for TV, this "PseudoChannel" wouldn't be as cool as it is. I look forward to tinkering with this project and seeing others "unplugging" and creating their own home network. Mark has some excellent ideas in regard to making this thing much more usable as a "pseudo-cable" network - I think this will be in the next version as it is the 'icing on the cake' sort of feature. Anyway, enjoy!

128
bash-scripts/channeldown.sh Normal file
View File

@@ -0,0 +1,128 @@
#!/bin/bash
# file: channeldown.sh
#----
# Simple script to cycle through multiple pseudo-channel instances - triggering start / stop.
#----
#----
# To Use:
# Configure something (a tv remote or alexa) to trigger this script. Make sure you move this script just
# outside of the pseudo-channel directories:
# -------------------
# -channels/
# --pseudo-channel_1/
# ---startstop.sh
# --pseudo-channel_2/
# ---startstop.sh
# --pseudo-channel_3/
# ---startstop.sh
# --channeldown.sh <--- on the same level as the 3 channels.
#----
# Make sure that each channel dir ends with a "_" + an incrementing number as seen above.
#----BEGIN EDITABLE VARS----
SCRIPT_TO_EXECUTE='startstop.sh'
OUTPUT_PREV_CHANNEL_PATH=.
OUTPUT_PREV_CHANNEL_FILE=".prevplaying"
CHANNEL_DIR_INCREMENT_SYMBOL="_"
#----END EDITABLE VARS-------
FIRST_RUN=false
# Scan the dir to see how many channels there are, store them in an arr.
CHANNEL_DIR_ARR=( $(find . -maxdepth 1 -type d -name '*'"$CHANNEL_DIR_INCREMENT_SYMBOL"'[[:digit:]]' -printf "%P\n") )
# If the previous channel txt file doesn't exist already create it (first run?)
if [ ! -e "$OUTPUT_PREV_CHANNEL_PATH/$OUTPUT_PREV_CHANNEL_FILE" ]; then
#FIRST_RUN_NUM=$((${#CHANNEL_DIR_ARR[@]}))
echo 1 > "$OUTPUT_PREV_CHANNEL_PATH/$OUTPUT_PREV_CHANNEL_FILE"
echo "First run: $FIRST_RUN_NUM"
FIRST_RUN=true
fi
# If this script see's there are multiple channels,
# then read file, get prevchannel, increment, and trigger next channel:
if [ "${#CHANNEL_DIR_ARR[@]}" -gt 1 ]; then
NEXT_CHANNEL=""
NEXT_CHANNEL_NUM=1
PREV_CHANNEL_FOUND=false
PREV_CHANNEL_DIR=""
echo "+++++ There are ${#CHANNEL_DIR_ARR[@]} channels detected."
PREV_CHANNEL=$(<$OUTPUT_PREV_CHANNEL_PATH/$OUTPUT_PREV_CHANNEL_FILE)
echo "+++++ It looks like the previous channel was: $PREV_CHANNEL"
# Now that the prevchannel is stored in a var, loop through channels and find prev channel & increment
for ((i = ${#CHANNEL_DIR_ARR[@]};i >= 1;i--)); do
NEXT_CHANNEL_NUM=$i
if [[ ${i} == *"$PREV_CHANNEL"* ]]; then
echo "+++++ Found previous channel, incrementing by 1."
PREV_CHANNEL_FOUND=true
PREV_CHANNEL_DIR=${CHANNEL_DIR_ARR[i-1]}
continue
fi
if [ "$PREV_CHANNEL_FOUND" = true ] ; then
NEXT_CHANNEL=${CHANNEL_DIR_ARR[i-1]}
break
fi
done
# If the next channel is an empty string, then we need to start the cycle over.
if [ -z "$NEXT_CHANNEL" ]; then
ARR_LENGTH=(${#CHANNEL_DIR_ARR[@]})
NEXT_CHANNEL=${CHANNEL_DIR_ARR[$ARR_LENGTH-1]}
echo "Starting cycle over."
echo "$PREV_CHANNEL_DIR"
echo "$NEXT_CHANNEL"
fi
echo "+++++ The next channel is: $NEXT_CHANNEL"
# Write next channel to previous channel file to reference later
echo "$NEXT_CHANNEL" | cut -d "_" -f2 > "$OUTPUT_PREV_CHANNEL_PATH/$OUTPUT_PREV_CHANNEL_FILE"
# Finally let's trigger the startstop script in both the previous channel and the next channel dirs.
# This will stop the previous channels playback & trigger the next channels playback
if [ "$FIRST_RUN" = false ]; then
cd "$OUTPUT_PREV_CHANNEL_PATH"/"$PREV_CHANNEL_DIR" && ./"$SCRIPT_TO_EXECUTE"
cd ../"$NEXT_CHANNEL" && ./"$SCRIPT_TO_EXECUTE"
else
cd "$OUTPUT_PREV_CHANNEL_PATH"/"$NEXT_CHANNEL" && ./"$SCRIPT_TO_EXECUTE"
fi
sleep 1
fi
exit 0

119
bash-scripts/channelup.sh Normal file
View File

@@ -0,0 +1,119 @@
#!/bin/bash
# file: channelup.sh
#----
# Simple script to cycle through multiple pseudo-channel instances - triggering start / stop.
#----
#----
# To Use:
# Configure something (a tv remote or alexa) to trigger this script. Make sure you move this script just
# outside of the pseudo-channel directories:
# -------------------
# -channels/
# --pseudo-channel_1/
# ---startstop.sh
# --pseudo-channel_2/
# ---startstop.sh
# --pseudo-channel_3/
# ---startstop.sh
# --channelup.sh <--- on the same level as the 3 channels.
#----
# Make sure that each channel dir ends with a "_" + an incrementing number as seen above.
#----BEGIN EDITABLE VARS----
SCRIPT_TO_EXECUTE='startstop.sh'
OUTPUT_PREV_CHANNEL_PATH=.
OUTPUT_PREV_CHANNEL_FILE=".prevplaying"
CHANNEL_DIR_INCREMENT_SYMBOL="_"
#----END EDITABLE VARS-------
FIRST_RUN=false
# If the previous channel txt file doesn't exist already create it (first run?)
if [ ! -e "$OUTPUT_PREV_CHANNEL_PATH/$OUTPUT_PREV_CHANNEL_FILE" ]; then
echo 1 > "$OUTPUT_PREV_CHANNEL_PATH/$OUTPUT_PREV_CHANNEL_FILE"
FIRST_RUN=true
fi
# If the file exists b
# Scan the dir to see how many channels there are, store them in an arr.
CHANNEL_DIR_ARR=( $(find . -maxdepth 1 -type d -name '*'"$CHANNEL_DIR_INCREMENT_SYMBOL"'[[:digit:]]' -printf "%P\n") )
# If this script see's there are multiple channels,
# then read file, get prevchannel, increment, and trigger next channel:
if [ "${#CHANNEL_DIR_ARR[@]}" -gt 1 ]; then
NEXT_CHANNEL=""
PREV_CHANNEL_FOUND=false
PREV_CHANNEL_DIR=""
echo "+++++ There are ${#CHANNEL_DIR_ARR[@]} channels detected."
PREV_CHANNEL=$(<$OUTPUT_PREV_CHANNEL_PATH/$OUTPUT_PREV_CHANNEL_FILE)
echo "+++++ It looks like the previous channel was: $PREV_CHANNEL"
# Now that the prevchannel is stored in a var, loop through channels and find prev channel & increment
for channel in "${CHANNEL_DIR_ARR[@]}"
do
if [[ $channel == *"$PREV_CHANNEL"* ]]; then
echo "+++++ Found previous channel, incrementing by 1."
PREV_CHANNEL_FOUND=true
PREV_CHANNEL_DIR=$channel
continue
fi
if [ "$PREV_CHANNEL_FOUND" = true ] ; then
NEXT_CHANNEL=$channel
break
fi
done
# If the next channel is an empty string, then we need to start the cycle over.
if [ -z "$NEXT_CHANNEL" ]; then
NEXT_CHANNEL=${CHANNEL_DIR_ARR[0]}
fi
echo "+++++ The next channel is: $NEXT_CHANNEL"
# Write next channel to previous channel file to reference later
echo "$NEXT_CHANNEL" | cut -d "_" -f2 > "$OUTPUT_PREV_CHANNEL_PATH/$OUTPUT_PREV_CHANNEL_FILE"
# Finally let's trigger the startstop script in both the previous channel and the next channel dirs.
# This will stop the previous channels playback & trigger the next channels playback
if [ "$FIRST_RUN" = false ]; then
cd "$OUTPUT_PREV_CHANNEL_PATH"/"$PREV_CHANNEL_DIR" && ./"$SCRIPT_TO_EXECUTE"
cd ../"$NEXT_CHANNEL" && ./"$SCRIPT_TO_EXECUTE"
else
cd "$OUTPUT_PREV_CHANNEL_PATH"/"$NEXT_CHANNEL" && ./"$SCRIPT_TO_EXECUTE"
fi
sleep 1
fi
exit 0

View File

@@ -0,0 +1,13 @@
#!/bin/bash
# file: generate-channels-daily-schedules.sh
#----
# Simple script to generate the daily schedule for each individual channel.
#
#----
#----
# To Use:
# This script needs to be setup with a crontab entry that runs everyday at midnight.
#----

View File

@@ -0,0 +1,70 @@
#!/bin/bash
# file: updatechannels.sh
#----
# Simple script to updates each channels local db with new Plex lib items / xml.
#----
#----
# To Use:
# If you added new content to your Plex Library, just make this file executable move it
# to where the plex_token.py file is and run ./updatechannels.sh
#----
# Make sure that each channel dir ends with a "_" + an incrementing number as seen above.
#----BEGIN EDITABLE VARS----
SCRIPT_TO_EXECUTE_PLUS_ARGS='PseudoChannel.py -u -xml'
OUTPUT_PREV_CHANNEL_PATH=.
CHANNEL_DIR_INCREMENT_SYMBOL="_"
PYTHON_TO_USE="$(which python)"
# If using 'virtualenv' with python, specify the local virtualenv dir.
VIRTUAL_ENV_DIR="env"
#----END EDITABLE VARS-------
# If virtualenv specified & exists, using that version of python instead.
if [ -d "$VIRTUAL_ENV_DIR" ]; then
PYTHON_TO_USE="$VIRTUAL_ENV_DIR/bin/python"
fi
# If the file exists b
# Scan the dir to see how many channels there are, store them in an arr.
CHANNEL_DIR_ARR=( $(find . -maxdepth 1 -type d -name '*'"$CHANNEL_DIR_INCREMENT_SYMBOL"'[[:digit:]]' -printf "%P\n") )
# If this script see's there are multiple channels,
# then loop through each channel and run the updates
if [ "${#CHANNEL_DIR_ARR[@]}" -gt 1 ]; then
# If virtualenv specified & exists, using that version of python instead.
if [ -d ./"$channel"/"$VIRTUAL_ENV_DIR" ]; then
PYTHON_TO_USE=./"$channel"/"$VIRTUAL_ENV_DIR/bin/python"
fi
echo "+++++ There are ${#CHANNEL_DIR_ARR[@]} channels detected."
for channel in "${CHANNEL_DIR_ARR[@]}"
do
echo "+++++ Trying to update: ""$PYTHON_TO_USE" ./"$channel"/$SCRIPT_TO_EXECUTE_PLUS_ARGS
# If the running.pid file doesn't exists, create it, start PseudoChannel.py and add the PID to it.
"$PYTHON_TO_USE" ./"$channel"/$SCRIPT_TO_EXECUTE_PLUS_ARGS
sleep 1
done
fi
exit 0

4
plex_token-example.py Normal file
View File

@@ -0,0 +1,4 @@
'''Change the values below, rename this file to "plex_token.py", and move this file just
outside of this directory ("../pseudo-channel")'''
token = "<your token>"
baseurl = 'http://media.home:32400'

View File

@@ -5,7 +5,7 @@
touch ../plex_token.py
2) add this line to the newly created file:
2) add these lines to the newly created file:
baseurl = 'the url to your server'
token = 'your plex token'
@@ -20,7 +20,9 @@
"Movies" : ["Films"],
6) For Google Calendar integration add your "gkey" to the "plex_token.py" file
6) *Skip this feature for now*
For Google Calendar integration add your "gkey" to the "plex_token.py" file
...(https://docs.simplecalendar.io/find-google-calendar-id/):
gkey = "the key"
@@ -31,6 +33,65 @@
"""
'''
*
* List of plex clients to use (add multiple clients to control multiple TV's)
*
'''
plexClients = ['RasPlex']
plexLibraries = {
"TV Shows" : ["TV Shows"],
"Movies" : ["Movies"],
"Commercials" : ["Commercials"],
}
useCommercialInjection = True
"""How many seconds to pad commercials between each other / other media"""
commercialPadding = 5
"""
Specify the path to this controller on the network (i.e. 'http://192.168.1.28' - no trailing slash).
Also specify the desired port to run the simple http webserver. The daily generated
schedule will be served at "http://<your-ip>:<your-port>/" (i.e. "http://192.168.1.28:8000/").
You can also leave the below controllerServerPath empty if you'd like to run your own webserver.
"""
controllerServerPath = "http://192.168.1.28"
controllerServerPort = "8000"
"""
When the schedule updates every 24 hours, it's possible that it will interrupt any shows / movies that were
playing from the previous day. To fix this, the app saves a "cached" schedule from the previous day to
override any media that is trying to play while the previous day is finishing.
"""
useDailyOverlapCache = True
dailyUpdateTime = "12:00 AM"
"""---"""
useGoogleCalendar = False
"""When to delete / remake the pseudo-channel.log - right at midnight, (i.e. 'friday') """
rotateLog = "friday"
"""Debug mode will give you more output in your terminal to help problem solve issues."""
debug_mode = True
"""
##### Do not edit below this line---------------------------------------------------------------
Below is logic to grab your Plex 'token' & Plex 'baseurl'. If you are following along and have created a 'plex_token.py'
file as instructed, you do not need to edit below this line.
"""
import os, sys
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
# import ../plex_token.py
@@ -43,39 +104,4 @@ except ImportError as e:
baseurl = plex_token.baseurl
token = plex_token.token
gkey = plex_token.gkey
'''
*
* List of plex clients to use (add multiple clients to control multiple TV's)
*
'''
plexClients = ['RasPlex']
plexLibraries = {
"TV Shows" : ["TV Shows"],
"Movies" : ["Movies"],
"Music" : ["Music"],
"Commercials" : ["Commercials"],
}
useGoogleCalendar = False
useCommercialInjection = True
# How many seconds to pad commercials between each other / other media
commercialPadding = 5
"""
Specify the path to this controller on the network (i.e. 'http://192.168.1.28' - no trailing slash).
Also specify the desired port to run the simple http webserver. The daily generated
schedule will be served at "http://<your-ip>:<your-port>/" (i.e. "http://192.168.1.28:8000/").
You can also leave the below controllerServerPath empty if you'd like to run your own webserver.
"""
controllerServerPath = "http://192.168.1.28"
controllerServerPort = "8000"
dailyUpdateTime = "12:00 AM"
debug_mode = True
gkey = '' #plex_token.gkey

View File

@@ -1,97 +1,279 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Welcome to PseudoChannel!
This will be the most difficult part in the process of setting up your PseudoChannel (but it is not hard,
just be sure to read this and you will be set).
There are a few things to keep in mind when setting up this XML.
1) What exactly is this file for?
The whole idea behind "PseudoChannel.py" is to create your own channel(s) to mimick real TV,
using your own "TV Shows", "Movies" and "Commercials". You are not supposed to intervene too often,
rather you are supposed to set it up (here) and just let it go as it advances in series episodes, playing
commercials to fill up gaps between scheduled content and playing movies where specified. That being said,
this particular file is where you map out what your channel looks like. I have my own schedule
below so you can reference it when building your own channel.
2) How do I make sense of these '<time></time>' blocks / '<weekdays></weekdays' blocks?
Since the whole concept of the channel is to have repeating blocks of content that changes throughout the
week depending on the day / time of day (like a real channel) you specify your "TV Shows" and "Movies" schedule
by adding a '<time></time>' block within the part of the week you want it to be scheduled for. For instance,
below I have "Looney Tunes" scheduled to play "everyday" starting at "6:00 AM", whereas I have "Garfield &#38; Friends"
playing only on "weekday" mornings scheduled for after Looney Tunes starting at "8:00 AM". Also notice that
"Garfield &#38; Friends" below is actually written as, "Garfield &#38; Friends". This is especially important
to those new to editing XML. In XML, "UTF-8", you are forbidden from using certain characters like the and
character. It is important to encode your titles to XML friendly text (this is also important for non-english characters).
You can convert titles that have forbidden characters by using this online too: http://coderstoolbox.net/string/#!encoding=xml&action=encode&charset=us_ascii
3) Setting the available parameters: "title=", "type=", "strict-time=", "time-shift=", "xtra="
There are two required parameters: "title" and "type". The "title" value should be either the title of your series
(i.e. "Friends") or if you are scheduling a movie it should only be set to, "random". The "type" parameter should be set
to either "series" or "movie". The attribute "strict-time" can either be "true" or "false" and refers to
whether or not the particular "<time>" block will be scheduled for the exact time you specify or if it will
shift around to fill up gaps. This is useful as sometimes episodes are as short as 5 minutes (cartoons) while
other episodes that are normally ~25 minutes are an hour or so long. Setting "strict-time" to "false" will
tell the app to shift that time block closer to the previous episode. The corresponding, "time-shift" attribute
tells the app how to shift the item. Its value can be "1" or more and will help the scheduler determine when to schedule the
shifting time according to that value. So for instance, if you'd like no gaps between your content, then you want
to set "strict-time='false'" and "time-shift='1'". However if you want your content to shift but would rather
it 'hook' on to a pretty time, like "2:45 PM" versus "2:41 PM" then you would set "time-shift" to a value like "5".
This will shift content around and schedule it within 5 minutes of the previous item but hook it on to the
nearest multiple of "5". You could use "15" or "30" too for even prettier times. Experiment.
4) Movies. How do I schedule "Billy Madison" to play on Saturday afternoon?
Well, since the app is supposed to work like a real TV Channel, you aren't supposed to have that kind of
control. If you want to watch "Billy Madison" then why not just turn on your Plex TV app and play it? Instead
here you want to always use "random" tor the "title=" value of movie content. But let's say you have a ton of Adam Sandler
movies and want to schedule a "random" Adam Sandler movie on Saturday afternoon? That makes more sense, that way
you aren't playing the same movie every Saturday afternoon! For movies specifically, you have a new
attribute called "xtra". There you can add various parameters to narrow in on the random movie type you
want scheduled every Saturday afternoon. So if for some reason you are set on playing an Adam Sandler comedy
every Saturday, then you might have a <time> block that looks like this:
<time title="random" type="movie" strict-time="true" xtra='actor:adam sandler genre:comedy contentRating:PG'>12:45 PM</time>
The available "xtra" paramters are as follows (http://python-plexapi.readthedocs.io/en/latest/_modules/plexapi/library.html#LibrarySection.search):
* unwatched: Display or hide unwatched content (True, False). [all]
* duplicate: Display or hide duplicate items (True, False). [movie]
* actor: List of actors to search ([actor_or_id, ...]). [movie]
* collection: List of collections to search within ([collection_or_id, ...]). [all]
* contentRating: List of content ratings to search within ([rating_or_key, ...]). [movie]
* country: List of countries to search within ([country_or_key, ...]). [movie,music]
* decade: List of decades to search within ([yyy0, ...]). [movie]
* director: List of directors to search ([director_or_id, ...]). [movie]
* genre: List Genres to search within ([genere_or_id, ...]). [all]
* resolution: List of video resolutions to search within ([resolution_or_key, ...]). [movie]
* studio: List of studios to search within ([studio_or_key, ...]). [music]
* year: List of years to search within ([yyyy, ...]). [all]
Currently the "xtra" attribute is only available to be used with movies.
5) Commercials?
If you are planning on using "time-shift" with a value greater than "1", then you will have empty gaps
in between your scheduled content. A neat feature is to fill those gaps with commercials, music videos,
or whatever you can come up with. All you have to do is set the commercial flag in the "pseudo_config.py"
file to tell the app to use "commercial injection" and make sure you have a "Commercials" library in your
plex media library. In that library, fill it with as many commercials or short videos as you can. The more
the better! I have close to a thousand commercials in mine - this helps the app fill up the gaps with a
wide variety of video content of varied durations. (hint: use a tool like 'youtube-dl' to download full
playlists from yourtube. You can fill up your "Commercials" library quick). Once you have your commercials library
setup, make sure to run, "python PseudoChannel.py -u" once more to update your local db with your new commercials
library. Commercials will now be "injected" to fill up gaps upon the next days schedule (or you can manually
generate the schedule using the "-g" flag).
Ok, that should be it. I've made it sound much more complicated than it actually is. Just make sure that you aren't
accidentally overlapping times, aren't accidentally trying to use forbidden XML characters, etc. Once you have
everything set, it should be hands off form there on out. Just go back to the cli and run "python PseudoChannel.py -xml"
to tell the app that you have updated the XML.
Oh and lastly, make sure that your "series" title's are written exactly as they are in your Plex Library. So if you
have "The Office (us)" in your Plex library, you need to have it written exactly like that here (not case sensitive) or it won't work. In
my previous "garfield" example you might be tempted to write it as "Garfield and Friends" instead of the hassle of
using the XML ascii character "&#38;". Well you cannot do that. I usually like to have my Plex Server web page
open in a tab while making my XML. That way for each "series" title I can double check the library to make sure I
am using the series title exactly as Plex is.
Ok, that is it. If you have questions feel free to e-mail me at justin@pseudochannel.tv or open an 'issue' on the
github repository. Have fun!
Cheers!
-->
<schedule>
<everyday>
<time title="Looney Tunes" type="series" strict-time="true" time-shift="1" overlap-max="">6:00 AM</time>
<time title="Looney Tunes" type="series" strict-time="false" time-shift="1" overlap-max="">6:30 AM</time>
<time title="Looney Tunes" type="series" strict-time="false" time-shift="1" overlap-max="">7:00 AM</time>
<time title="Looney Tunes" type="series" strict-time="false" time-shift="1" overlap-max="">7:30 AM</time>
<!-- The first item is set to: strict-time=true - this isn't necessary but helps organize the schedule -->
<!--
The following is a good example of content that is extremely short. Looney Tunes cartoons are around
5 minutes in length per episode. Althrough sometimes they are as much as 25 minutes long.
Since strict-time is set to false, the app will adjust accordingly. I am however setting a *rough
intended time of 6:00 AM and incrementing by 10 minutes. The app will figure out the real
start times but this will help me conceptualize my schedule as I keep adding entries.
-->
<time title="Looney Tunes" type="series" strict-time="false" time-shift="1" >6:00 AM</time>
<time title="Looney Tunes" type="series" strict-time="false" time-shift="1" >6:10 AM</time>
<time title="Looney Tunes" type="series" strict-time="false" time-shift="1" >6:20 AM</time>
<time title="Looney Tunes" type="series" strict-time="false" time-shift="1" >6:40 AM</time>
<time title="Looney Tunes" type="series" strict-time="false" time-shift="1" >6:50 AM</time>
</everyday>
<mondays></mondays>
<tuesdays></tuesdays>
<wednesdays></wednesdays>
<thursdays></thursdays>
<fridays>
<!--
Here is an example of a day specific block. TGIF 90's style. Again, I am keeping strict-time set to false
to maximize the content in my schedule and to avoid overlap. If you wanted to make sure your TGIF block
started at 8 PM on the dot, you could make room for it...
-->
<time title="Sabrina The Teenage Witch" type="series" strict-time="false" time-shift="1" >8:00 PM</time>
<time title="Boy Meets World" type="series" strict-time="false" time-shift="1" >8:20 PM</time>
<time title="Family Matters" type="series" strict-time="false" time-shift="1" >8:40 PM</time>
<time title="Family Matters" type="series" strict-time="false" time-shift="1" >9:00 PM</time>
</fridays>
<saturdays></saturdays>
<sundays></sundays>
<saturdays>
<!--
Here is an example of using the xtra attr to specify a saturday late-afternoon 80's movie block.
-->
<time title="random" type="movie" strict-time="false" time-shift="5" xtra="decade:1980">5:00 PM</time>
<time title="Elementary" type="show" strict-time="false" time-shift="5" >7:00 PM</time>
</saturdays>
<sundays>
<!--
Here is an example of using the xtra attr to specify a sunday late-afternoon romantic comedy movie block.
-->
<time title="random" type="movie" strict-time="false" time-shift="5" xtra="genre:comedy,romance">5:00 PM</time>
<time title="Sherlock" type="show" strict-time="false" time-shift="5" >7:00 PM</time>
</sundays>
<weekends>
<time title="Garfield &#38; Friends" type="series" strict-time="false" time-shift="5" overlap-max="">8:00 AM</time>
<time title="Garfield &#38; Friends" type="series" strict-time="false" time-shift="5" overlap-max="">8:30 AM</time>
<time title="Garfield &#38; Friends" type="series" strict-time="false" time-shift="5" overlap-max="">9:00 AM</time>
<time title="Garfield &#38; Friends" type="series" strict-time="false" time-shift="5" overlap-max="">9:30 AM</time>
<!--
I am incrementing the start times by about 30 minutes. Since times will shift with a
5 minute "time-shift", the app will adjust the schedule according to the episode
duration / time-shift value.
-->
<time title="The Smurfs" type="series" strict-time="false" time-shift="5" >7:00 AM</time>
<time title="Batman" type="series" strict-time="false" time-shift="5" overlap-max="">10:00 AM</time>
<time title="Batman" type="series" strict-time="false" time-shift="5" overlap-max="">10:30 AM</time>
<time title="Garfield &#38; Friends" type="series" strict-time="false" time-shift="5" >7:30 AM</time>
<time title="random" type="movie" strict-time="false" time-shift="5" overlap-max="">11:00 AM</time>
<time title="Batman" type="series" strict-time="false" time-shift="5" >8:00 AM</time>
<time title="The Adventures of Pete &#38; Pete" type="series" strict-time="false" time-shift="5" overlap-max="">12:30 PM</time>
<time title="The Adventures of Pete &#38; Pete" type="series" strict-time="false" time-shift="5" overlap-max="">1:00 PM</time>
<time title="Gilligan's Island" type="series" strict-time="false" time-shift="5" overlap-max="">1:30 PM</time>
<time title="Gilligan's Island" type="series" strict-time="false" time-shift="5" overlap-max="">1:40 PM</time>
<time title="The Jetsons" type="series" strict-time="false" time-shift="5" overlap-max="">2:00 PM</time>
<time title="random" type="movie" strict-time="false" time-shift="5" xtra="genre:comedy">8:30 AM</time>
<time title="random" type="movie" strict-time="true" time-shift="5" overlap-max="">2:30 PM</time>
<time title="The Adventures of Pete &#38; Pete" type="series" strict-time="false" time-shift="5" >10:00 AM</time>
<time title="The Wonder Years" type="series" strict-time="false" time-shift="5" overlap-max="">4:30 PM</time>
<time title="The Wonder Years" type="series" strict-time="false" time-shift="5" overlap-max="">5:00 PM</time>
<time title="Gilligan's Island" type="series" strict-time="false" time-shift="5" >10:30 AM</time>
<time title="The Simpsons" type="series" strict-time="true" time-shift="5" overlap-max="">5:30 PM</time>
<time title="The Simpsons" type="series" strict-time="false" time-shift="5" overlap-max="">6:00 PM</time>
<time title="The Jetsons" type="series" strict-time="false" time-shift="5" >11:00 AM</time>
<time title="Sherlock" type="series" strict-time="false" time-shift="5" overlap-max="">6:30 PM</time>
<time title="Blossom" type="series" strict-time="false" time-shift="5" >11:30 AM</time>
<time title="Arrested Development" type="series" strict-time="false" time-shift="5" overlap-max="">7:00 PM</time>
<time title="Arrested Development" type="series" strict-time="false" time-shift="5" overlap-max="">7:30 PM</time>
<time title="Clarissa Explains It All" type="series" strict-time="false" time-shift="5" >12:00 AM</time>
<time title="random" type="movie" strict-time="false" time-shift="5" overlap-max="">8:00 PM</time>
<time title="random" type="movie" strict-time="false" time-shift="5" xtra="genre:action decade:1990,1980,1970,1960">12:30 PM</time>
<time title="Band of Brothers" type="show" strict-time="false" time-shift="5" overlap-max="">10:00 PM</time>
<time title="Beverly Hills, 90210" type="show" strict-time="false" time-shift="5" overlap-max="">10:30 PM</time>
<time title="The Wonder Years" type="series" strict-time="false" time-shift="5" >2:00 PM</time>
<time title="The Simpsons" type="series" strict-time="false" time-shift="5" >2:30 PM</time>
<time title="Monk" type="series" strict-time="false" time-shift="5" >3:00 PM</time>
<time title="Married... with Children" type="series" strict-time="false" time-shift="5" >3:30 PM</time>
<time title="The Fresh Prince of Bel-Air" type="series" strict-time="false" time-shift="5" >4:00 PM</time>
<time title="Arrested Development" type="series" strict-time="false" time-shift="5" >4:30 PM</time>
<!--
Here I am leaving some room for saturdays/sundays specific content. I will keep strict-time false for all my
times so I can squeeze in as much content into my schedule without cutting anything off. If you choose to
use strict-time, then it's important to make sure you have left a good amount of room so scheduled content
doesn't get cutoff.
-->
<time title="Beverly Hills, 90210" type="show" strict-time="false" time-shift="5" >8:00 PM</time>
<time title="Daria" type="show" strict-time="false" time-shift="5" >9:00 PM</time>
<time title="The Scooby-Doo Show" type="show" strict-time="false" time-shift="5" >9:30 PM</time>
<time title="The Flintstones" type="show" strict-time="false" time-shift="5" >10:00 PM</time>
<time title="Happy Days" type="show" strict-time="false" time-shift="5" >10:30 PM</time>
<time title="The Flintstones" type="show" strict-time="false" time-shift="5" overlap-max="">11:00 PM</time>
<time title="The Flintstones" type="show" strict-time="false" time-shift="5" overlap-max="">11:30 PM</time>
</weekends>
<weekdays>
<default title="Seinfeld" type="series" ></default>
<time title="Garfield &#38; Friends" type="series" strict-time="false" time-shift="5" >8:00 AM</time>
<time title="Garfield &#38; Friends" type="series" strict-time="false" time-shift="5" >8:30 AM</time>
<time title="Garfield &#38; Friends" type="series" strict-time="false" time-shift="5" >9:00 AM</time>
<time title="Garfield &#38; Friends" type="series" strict-time="false" time-shift="5" >9:30 AM</time>
<time title="Garfield &#38; Friends" type="series" strict-time="false" time-shift="5" overlap-max="">8:00 AM</time>
<time title="Garfield &#38; Friends" type="series" strict-time="false" time-shift="5" overlap-max="">8:30 AM</time>
<time title="Garfield &#38; Friends" type="series" strict-time="false" time-shift="5" overlap-max="">9:00 AM</time>
<time title="Garfield &#38; Friends" type="series" strict-time="false" time-shift="5" overlap-max="">9:30 AM</time>
<time title="talespin" type="series" strict-time="false" time-shift="5" >10:00 AM</time>
<time title="talespin" type="series" strict-time="false" time-shift="5" >10:30 AM</time>
<time title="talespin" type="series" strict-time="false" time-shift="5" overlap-max="">10:00 AM</time>
<time title="talespin" type="series" strict-time="false" time-shift="5" overlap-max="">10:30 AM</time>
<time title="macgyver" type="series" strict-time="false" time-shift="5" >11:00 AM</time>
<time title="macgyver" type="series" strict-time="false" time-shift="5" overlap-max="">11:00 AM</time>
<time title="boy meets world" type="series" strict-time="false" time-shift="5" >12:00 PM</time>
<time title="boy meets world" type="series" strict-time="false" time-shift="5" overlap-max="">12:00 PM</time>
<time title="full house" type="series" strict-time="false" time-shift="5" >12:30 PM</time>
<time title="full house" type="series" strict-time="false" time-shift="5" >1:00 PM</time>
<time title="full house" type="series" strict-time="false" time-shift="5" overlap-max="">12:30 PM</time>
<time title="full house" type="series" strict-time="false" time-shift="5" overlap-max="">1:00 PM</time>
<time title="The Wonder Years" type="series" strict-time="false" time-shift="5" >1:30 PM</time>
<time title="the it crowd" type="series" strict-time="false" time-shift="5" overlap-max="">1:30 PM</time>
<time title="the it crowd" type="series" strict-time="false" time-shift="5" overlap-max="">2:00 PM</time>
<time title="The Simpsons" type="series" strict-time="false" time-shift="5" >2:00 PM</time>
<time title="the office (us)" type="series" strict-time="false" time-shift="5" overlap-max="">2:30 PM</time>
<time title="the office (us)" type="series" strict-time="false" time-shift="5" overlap-max="">3:00 PM</time>
<time title="Monk" type="series" strict-time="false" time-shift="5" >2:30 PM</time>
<time title="friends" type="series" strict-time="false" time-shift="5" overlap-max="">3:30 PM</time>
<time title="friends" type="series" strict-time="false" time-shift="5" overlap-max="">4:00 PM</time>
<time title="Married... with Children" type="series" strict-time="false" time-shift="5" >3:00 PM</time>
<time title="seinfeld" type="series" strict-time="false" time-shift="5" overlap-max="">4:30 PM</time>
<time title="seinfeld" type="series" strict-time="false" time-shift="5" overlap-max="">5:00 PM</time>
<time title="The Fresh Prince of Bel-Air" type="series" strict-time="false" time-shift="5" >3:30 PM</time>
<time title="Futurama" type="series" strict-time="false" time-shift="5" overlap-max="">5:30 PM</time>
<time title="the office (us)" type="series" strict-time="false" time-shift="5" >4:00 PM</time>
<time title="the office (us)" type="series" strict-time="false" time-shift="5" >4:30 PM</time>
<time title="Saved by the Bell" type="series" strict-time="false" time-shift="5" overlap-max="">6:00 PM</time>
<time title="Saved by the Bell" type="series" strict-time="false" time-shift="5" overlap-max="">6:30 PM</time>
<time title="Roseanne" type="series" strict-time="false" time-shift="5" >5:00 PM</time>
<time title="Roseanne" type="series" strict-time="false" time-shift="5" >5:30 PM</time>
<time title="new girl" type="series" strict-time="false" time-shift="5" overlap-max="">7:00 PM</time>
<time title="new girl" type="series" strict-time="false" time-shift="5" overlap-max="">7:30 PM</time>
<time title="seinfeld" type="series" strict-time="false" time-shift="5" >6:00 PM</time>
<time title="Parker Lewis Can&#39;t Lose" type="series" strict-time="false" time-shift="5" >6:30 PM</time>
<time title="Futurama" type="series" strict-time="false" time-shift="5" >7:00 PM</time>
<time title="Saved by the Bell" type="series" strict-time="false" time-shift="5" >7:30 PM</time>
<time title="Saved by the Bell" type="series" strict-time="false" time-shift="5" >8:00 PM</time>
<time title="new girl" type="series" strict-time="false" time-shift="5" >8:30 PM</time>
<time title="random" type="movie" strict-time="false" time-shift="5" xtra='actor:mike myers genre:comedy contentRating:PG-13'>9:00 PM</time>
<time title="the trip" type="series" strict-time="false" time-shift="5" overlap-max="">8:30 PM</time>
<time title="the trip" type="series" strict-time="false" time-shift="5" overlap-max="">9:00 PM</time>
<time title="Married... with Children" type="series" strict-time="false" time-shift="5" >9:30 PM</time>
<time title="Married... with Children" type="series" strict-time="false" time-shift="5" >9:40 PM</time>
<time title="Blossom" type="series" strict-time="false" time-shift="5" >10:00 PM</time>
<time title="Frasier" type="series" strict-time="false" time-shift="1" >10:20 PM</time>
<time title="Mad About You" type="series" strict-time="false" time-shift="5" >10:30 PM</time>
<time title="Mad About You" type="series" strict-time="false" time-shift="5" >10:40 PM</time>
<time title="M*A*S*H" type="series" strict-time="false" time-shift="5" >10:50 PM</time>
<time title="Beverly Hills, 90210" type="series" strict-time="false" time-shift="5" >11:00 PM</time>
<time title="Cheers" type="series" strict-time="false" time-shift="5" >11:20 PM</time>
<time title="Three&#39;s Company" type="series" strict-time="false" time-shift="5" >11:30 PM</time>
<time title="The Brady Bunch" type="series" strict-time="false" time-shift="5" >11:40 PM</time>
</weekdays>
</schedule>

View File

@@ -8,8 +8,10 @@ idna==2.5
kombu==4.1.0
oauth2client==4.1.2
PlexAPI==2.0.2
py==1.4.34
pyasn1==0.3.2
pyasn1-modules==0.0.11
pytest==3.2.1
pytz==2017.2
requests==2.18.3
rsa==3.4.2

43
src/Helpers.py Normal file
View File

@@ -0,0 +1,43 @@
import os
import logging
class Helpers():
"""Class for consolidating helper methods"""
def __init__(self):
pass
def save_file(self, data, filename, path, overwrite=True):
fileName = filename
writepath = path
if not os.path.exists(writepath):
os.makedirs(writepath)
if os.path.exists(writepath+fileName) and overwrite:
os.remove(writepath+fileName)
mode = 'a' if os.path.exists(writepath) else 'w'
with open(writepath+fileName, mode) as f:
f.write(data)
def get_file(self, filename, path):
if not os.path.exists(writepath):
raise IOError("{}, doesn't exist").format(writepath)
if not os.path.exists(writepath+fileName):
raise IOError("{}, doesn't exist").format(fileName)
return None

View File

@@ -16,147 +16,86 @@ class PseudoChannelCommercial():
def __init__(self, commercials, commercialPadding):
self.commercials = commercials
self.COMMERCIAL_PADDING_IN_SECONDS = commercialPadding
def get_commercials_to_inject(self):
self.go()
return None
def get_random_commercial(self):
random_commercial = random.choice(self.commercials)
random_commercial_dur_seconds = (int(random_commercial[4])/1000)%60
while random_commercial_dur_seconds < self.MIN_DURATION_FOR_COMMERCIAL:
random_commercial = random.choice(self.commercials)
random_commercial_dur_seconds = (int(random_commercial[4])/1000)%60
return random_commercial
def go(self):
shuffled_commercial_list = copy.deepcopy(self.commercials)
random.shuffle(self.commercials, random.random)
#print shuffled_commercial_list
prev_item = None
for entry in self.daily_schedule:
"""First Episode"""
if prev_item == None:
prev_item = entry
else:
prev_item_end_time = datetime.datetime.strptime(prev_item[9], '%Y-%m-%d %H:%M:%S.%f')
curr_item_start_time = datetime.datetime.strptime(entry[8], '%I:%M:%S %p')
time_diff = (curr_item_start_time - prev_item_end_time)
days, hours, minutes = time_diff.days, time_diff.seconds // 3600, time_diff.seconds // 60 % 60
count = 0
commercial_list = []
commercial_dur_sum = 0
while int(time_diff.total_seconds()) >= commercial_dur_sum and count < len(self.commercials):
random_commercial = self.get_random_commercial()
commercial_list.append(random_commercial)
commercial_dur_sum += int(random_commercial[4])
print commercial_list
prev_item = entry
def timedelta_milliseconds(self, td):
return td.days*86400000 + td.seconds*1000 + td.microseconds/1000
def pad_the_commercial_dur(self, commercial):
commercial_as_list = list(commercial)
commercial_as_list[4] = int(commercial_as_list[4]) + (self.COMMERCIAL_PADDING_IN_SECONDS * 1000)
commercial = tuple(commercial_as_list)
return commercial
def get_commercials_to_place_between_media(self, last_ep, now_ep):
#print last_ep.end_time, now_ep.start_time
prev_item_end_time = datetime.strptime(last_ep.end_time.strftime('%Y-%m-%d %H:%M:%S.%f'), '%Y-%m-%d %H:%M:%S.%f')
curr_item_start_time = datetime.strptime(now_ep.start_time, '%I:%M:%S %p')
time_diff = (curr_item_start_time - prev_item_end_time)
count = 0
commercial_list = []
commercial_dur_sum = 0
time_diff_milli = self.timedelta_milliseconds(time_diff)
last_commercial = None
time_watch = prev_item_end_time
new_commercial_start_time = prev_item_end_time
#print "here", time_diff.seconds
while curr_item_start_time > new_commercial_start_time and (count) < len(self.commercials)*100:
random_commercial_without_pad = self.get_random_commercial()
"""
Padding the duration of commercials as per user specified padding.
"""
random_commercial = self.pad_the_commercial_dur(random_commercial_without_pad)
#new_commercial_seconds = (int(random_commercial[4])/1000)%60
new_commercial_milli = int(random_commercial[4])
if last_commercial != None:
#print last_commercial[3]
new_commercial_start_time = last_commercial.end_time
new_commercial_end_time = new_commercial_start_time + \
timedelta(milliseconds=int(new_commercial_milli))
else:
new_commercial_start_time = prev_item_end_time
new_commercial_end_time = new_commercial_start_time + \
timedelta(milliseconds=int(new_commercial_milli))
commercial_dur_sum += new_commercial_milli
formatted_time_for_new_commercial = new_commercial_start_time.strftime('%I:%M:%S %p')
new_commercial = Commercial(
"Commercials",
random_commercial[3],
@@ -169,15 +108,8 @@ class PseudoChannelCommercial():
"0", # overlap_max
"", # plex_media_id
)
last_commercial = new_commercial
if new_commercial_end_time > curr_item_start_time:
break
commercial_list.append(new_commercial)
#print "here!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"
return commercial_list

View File

@@ -9,83 +9,61 @@ class PseudoChannelDatabase():
def __init__(self, db):
self.db = db
self.conn = sqlite3.connect(self.db, check_same_thread=False)
self.cursor = self.conn.cursor()
"""Database functions.
Utilities, etc.
"""
def create_tables(self):
self.cursor.execute('CREATE TABLE IF NOT EXISTS '
'movies(id INTEGER PRIMARY KEY AUTOINCREMENT, '
'unix INTEGER, mediaID INTEGER, title TEXT, duration INTEGER, '
'lastPlayedDate TEXT, plexMediaID TEXT)')
self.cursor.execute('CREATE TABLE IF NOT EXISTS '
'videos(id INTEGER PRIMARY KEY AUTOINCREMENT, '
'unix INTEGER, mediaID INTEGER, title TEXT, duration INTEGER, plexMediaID TEXT)')
self.cursor.execute('CREATE TABLE IF NOT EXISTS '
'music(id INTEGER PRIMARY KEY AUTOINCREMENT, '
'unix INTEGER, mediaID INTEGER, title TEXT, duration INTEGER, plexMediaID TEXT)')
self.cursor.execute('CREATE TABLE IF NOT EXISTS '
'shows(id INTEGER PRIMARY KEY AUTOINCREMENT, '
'unix INTEGER, mediaID INTEGER, title TEXT, duration INTEGER, '
'lastEpisodeTitle TEXT, fullImageURL TEXT, plexMediaID TEXT)')
self.cursor.execute('CREATE TABLE IF NOT EXISTS '
'episodes(id INTEGER PRIMARY KEY AUTOINCREMENT, '
'unix INTEGER, mediaID INTEGER, title TEXT, duration INTEGER, '
'episodeNumber INTEGER, seasonNumber INTEGER, showTitle TEXT, plexMediaID TEXT)')
self.cursor.execute('CREATE TABLE IF NOT EXISTS '
'commercials(id INTEGER PRIMARY KEY AUTOINCREMENT, unix INTEGER, '
'mediaID INTEGER, title TEXT, duration INTEGER, plexMediaID TEXT)')
self.cursor.execute('CREATE TABLE IF NOT EXISTS '
'schedule(id INTEGER PRIMARY KEY AUTOINCREMENT, unix INTEGER, '
'mediaID INTEGER, title TEXT, duration INTEGER, startTime INTEGER, '
'endTime INTEGER, dayOfWeek TEXT, startTimeUnix INTEGER, section TEXT, '
'strictTime TEXT, timeShift TEXT, overlapMax TEXT)')
'strictTime TEXT, timeShift TEXT, overlapMax TEXT, xtra TEXT)')
self.cursor.execute('CREATE TABLE IF NOT EXISTS '
'daily_schedule(id INTEGER PRIMARY KEY AUTOINCREMENT, unix INTEGER, '
'mediaID INTEGER, title TEXT, episodeNumber INTEGER, seasonNumber INTEGER, '
'showTitle TEXT, duration INTEGER, startTime INTEGER, endTime INTEGER, '
'dayOfWeek TEXT, sectionType TEXT, plexMediaID TEXT)')
self.cursor.execute('CREATE TABLE IF NOT EXISTS '
'app_settings(id INTEGER PRIMARY KEY AUTOINCREMENT, version TEXT)')
#index
self.cursor.execute('CREATE UNIQUE INDEX IF NOT EXISTS idx_episode_title ON episodes (title);')
self.cursor.execute('CREATE UNIQUE INDEX IF NOT EXISTS idx_movie_title ON movies (title);')
self.cursor.execute('CREATE UNIQUE INDEX IF NOT EXISTS idx_shows_title ON shows (title);')
self.cursor.execute('CREATE UNIQUE INDEX IF NOT EXISTS idx_video_title ON videos (title);')
self.cursor.execute('CREATE UNIQUE INDEX IF NOT EXISTS idx_music_title ON music (title);')
self.cursor.execute('CREATE UNIQUE INDEX IF NOT EXISTS idx_commercial_title ON commercials (title);')
self.cursor.execute('CREATE UNIQUE INDEX IF NOT EXISTS idx_settings_version ON app_settings (version);')
"""Setting Basic Settings
"""
try:
self.cursor.execute("INSERT OR REPLACE INTO app_settings "
"(version) VALUES (?)",
("0.1",))
self.conn.commit()
# Catch the exception
except Exception as e:
@@ -101,38 +79,49 @@ class PseudoChannelDatabase():
pass
def drop_daily_schedule(self):
def drop_daily_schedule_table(self):
pass
sql = "DROP TABLE IF EXISTS daily_schedule"
self.cursor.execute(sql)
self.conn.commit()
def create_daily_schedule_table(self):
self.cursor.execute('CREATE TABLE IF NOT EXISTS '
'daily_schedule(id INTEGER PRIMARY KEY AUTOINCREMENT, unix INTEGER, '
'mediaID INTEGER, title TEXT, episodeNumber INTEGER, seasonNumber INTEGER, '
'showTitle TEXT, duration INTEGER, startTime INTEGER, endTime INTEGER, '
'dayOfWeek TEXT, sectionType TEXT, plexMediaID TEXT)')
self.conn.commit()
def remove_all_scheduled_items(self):
sql = "DELETE FROM schedule WHERE id > -1"
self.cursor.execute(sql)
self.conn.commit()
def remove_all_daily_scheduled_items(self):
sql = "DELETE FROM daily_schedule"
self.cursor.execute(sql)
self.conn.commit()
def clear_shows_table(self):
sql = "DELETE FROM shows"
self.cursor.execute(sql)
self.conn.commit()
"""Database functions.
Setters, etc.
"""
def add_movies_to_db(self, mediaID, title, duration, plexMediaID):
unix = int(time.time())
try:
self.cursor.execute("REPLACE INTO movies "
"(unix, mediaID, title, duration, plexMediaID) VALUES (?, ?, ?, ?, ?)",
(unix, mediaID, title, duration, plexMediaID))
self.conn.commit()
# Catch the exception
except Exception as e:
@@ -141,6 +130,7 @@ class PseudoChannelDatabase():
raise e
def add_videos_to_db(self, mediaID, title, duration, plexMediaID):
unix = int(time.time())
try:
self.cursor.execute("REPLACE INTO videos "
@@ -155,6 +145,7 @@ class PseudoChannelDatabase():
raise e
def add_shows_to_db(self, mediaID, title, duration, lastEpisodeTitle, fullImageURL, plexMediaID):
unix = int(time.time())
try:
self.cursor.execute("INSERT OR IGNORE INTO shows "
@@ -168,6 +159,7 @@ class PseudoChannelDatabase():
raise e
def add_episodes_to_db(self, mediaID, title, duration, episodeNumber, seasonNumber, showTitle, plexMediaID):
unix = int(time.time())
try:
self.cursor.execute("REPLACE INTO episodes "
@@ -181,6 +173,7 @@ class PseudoChannelDatabase():
raise e
def add_commercials_to_db(self, mediaID, title, duration, plexMediaID):
unix = int(time.time())
try:
self.cursor.execute("REPLACE INTO commercials "
@@ -194,13 +187,25 @@ class PseudoChannelDatabase():
self.conn.rollback()
raise e
def add_schedule_to_db(self, mediaID, title, duration, startTime, endTime, dayOfWeek, startTimeUnix, section, strictTime, timeShift, overlapMax):
def add_schedule_to_db(self,
mediaID,
title,
duration,
startTime,
endTime,
dayOfWeek,
startTimeUnix,
section,
strictTime,
timeShift,
overlapMax,
xtra):
unix = int(time.time())
try:
self.cursor.execute("REPLACE INTO schedule "
"(unix, mediaID, title, duration, startTime, endTime, dayOfWeek, startTimeUnix, section, strictTime, timeShift, overlapMax) "
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
(unix, mediaID, title, duration, startTime, endTime, dayOfWeek, startTimeUnix, section, strictTime, timeShift, overlapMax))
"(unix, mediaID, title, duration, startTime, endTime, dayOfWeek, startTimeUnix, section, strictTime, timeShift, overlapMax, xtra) "
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
(unix, mediaID, title, duration, startTime, endTime, dayOfWeek, startTimeUnix, section, strictTime, timeShift, overlapMax, xtra))
self.conn.commit()
# Catch the exception
except Exception as e:
@@ -224,9 +229,7 @@ class PseudoChannelDatabase():
):
unix = int(time.time())
try:
self.cursor.execute("INSERT OR REPLACE INTO daily_schedule "
"(unix, mediaID, title, episodeNumber, seasonNumber, "
"showTitle, duration, startTime, endTime, dayOfWeek, sectionType, plexMediaID) "
@@ -245,16 +248,11 @@ class PseudoChannelDatabase():
sectionType,
plexMediaID
))
self.conn.commit()
# Catch the exception
except Exception as e:
# Roll back any change if something goes wrong
self.conn.rollback()
raise e
def add_media_to_daily_schedule(self, media):
@@ -263,7 +261,6 @@ class PseudoChannelDatabase():
print str("#### Adding media to db: {} {}".format(media.title, media.start_time)).encode('UTF-8')
except:
print "----- Not outputting media info due to ascii code issues."
self.add_daily_schedule_to_db(
0,
media.title,
@@ -278,95 +275,94 @@ class PseudoChannelDatabase():
media.plex_media_id
)
"""Database functions.
def import_shows_table_by_row(self, mediaID, title, duration, lastEpisodeTitle, fullImageURL, plexMediaID):
unix = int(time.time())
try:
self.cursor.execute("REPLACE INTO shows "
"(unix, mediaID, title, duration, lastEpisodeTitle, fullImageURL, plexMediaID) VALUES (?, ?, ?, ?, ?, ?, ?)",
(unix, mediaID, title, duration, lastEpisodeTitle, fullImageURL, plexMediaID))
self.conn.commit()
# Catch the exception
except Exception as e:
# Roll back any change if something goes wrong
self.conn.rollback()
raise e
"""Database functions.
Getters, etc.
"""
def get_shows_table(self):
sql = "SELECT * FROM shows"
self.cursor.execute(sql)
return self.cursor.fetchall()
def get_media(self, title, mediaType):
media = mediaType
sql = "SELECT * FROM "+media+" WHERE (title LIKE ?) COLLATE NOCASE"
self.cursor.execute(sql, ("%"+title+"%", ))
media_item = self.cursor.fetchone()
return media_item
def get_schedule(self):
self.cursor.execute("SELECT * FROM schedule ORDER BY datetime(startTimeUnix) ASC")
datalist = list(self.cursor.fetchall())
return datalist
def get_daily_schedule(self):
print "##### Getting Daily Schedule from DB."
self.cursor.execute("SELECT * FROM daily_schedule ORDER BY datetime(startTime) ASC")
datalist = list(self.cursor.fetchall())
print "+++++ Done."
return datalist
def get_movie(self, title):
media = "movies"
return self.get_media(title, media)
def get_shows(self, title):
media = "shows"
return self.get_media(title, media)
def get_music(self, title):
media = "music"
return self.get_media(title, media)
def get_video(self, title):
media = "videos"
return self.get_media(title, media)
def get_episodes(self, title):
media = "episodes"
return self.get_media(title, media)
def get_commercials(self):
self.cursor.execute("SELECT * FROM commercials ORDER BY duration ASC")
datalist = list(self.cursor.fetchall())
return datalist
def update_shows_table_with_last_episode(self, showTitle, lastEpisodeTitle):
sql1 = "UPDATE shows SET lastEpisodeTitle = ? WHERE title LIKE ? COLLATE NOCASE"
self.cursor.execute(sql1, (lastEpisodeTitle, showTitle, ))
self.conn.commit()
def get_first_episode(self, tvshow):
sql = ("SELECT id, unix, mediaID, title, duration, MIN(episodeNumber), MIN(seasonNumber), "
"showTitle FROM episodes WHERE ( showTitle LIKE ?) COLLATE NOCASE")
self.cursor.execute(sql, (tvshow, ))
first_episode = self.cursor.fetchone()
return first_episode
'''
@@ -377,32 +373,24 @@ class PseudoChannelDatabase():
def get_episode_id(self, episodeTitle):
sql = "SELECT id FROM episodes WHERE ( title LIKE ?) COLLATE NOCASE"
self.cursor.execute(sql, (episodeTitle, ))
episode_id = self.cursor.fetchone()
return episode_id
def get_random_episode(self):
sql = "SELECT * FROM episodes WHERE id IN (SELECT id FROM episodes ORDER BY RANDOM() LIMIT 1)"
self.cursor.execute(sql)
return self.cursor.fetchone()
def get_random_movie(self):
sql = "SELECT * FROM movies WHERE id IN (SELECT id FROM movies ORDER BY RANDOM() LIMIT 1)"
self.cursor.execute(sql)
return self.cursor.fetchone()
def get_next_episode(self, series):
#print(series)
'''
*
* As a way of storing a "queue", I am storing the *next episode title in the "shows" table so I can
@@ -410,7 +398,6 @@ class PseudoChannelDatabase():
*
'''
self.cursor.execute("SELECT lastEpisodeTitle FROM shows WHERE title LIKE ? COLLATE NOCASE", (series, ))
last_title_list = self.cursor.fetchone()
'''
*
@@ -425,17 +412,13 @@ class PseudoChannelDatabase():
*
'''
first_episode = self.get_first_episode(series)
first_episode_title = first_episode[3]
#print(first_episode_title)
'''
*
* Add this episdoe title to the "shows" table for the queue functionality to work
*
'''
self.update_shows_table_with_last_episode(series, first_episode_title)
return first_episode
elif last_title_list:
@@ -444,10 +427,6 @@ class PseudoChannelDatabase():
* The last episode stored in the "shows" table was not empty... get the next episode in the series
*
'''
#print("First episode already set in shows, advancing episodes forward")
#print(str(self.get_episode_id(last_title_list[0])))
"""
*
* If this isn't a first run, then grabbing the next episode by incrementing id
@@ -455,7 +434,6 @@ class PseudoChannelDatabase():
"""
sql = ("SELECT * FROM episodes WHERE ( id > "+str(self.get_episode_id(last_title_list[0])[0])+
" AND showTitle LIKE ? ) ORDER BY seasonNumber LIMIT 1 COLLATE NOCASE")
self.cursor.execute(sql, (series, ))
'''
*
@@ -463,38 +441,23 @@ class PseudoChannelDatabase():
*
'''
next_episode = self.cursor.fetchone()
if next_episode != None:
#print(next_episode[3])
self.update_shows_table_with_last_episode(series, next_episode[3])
return next_episode
else:
print("Not grabbing next episode restarting series, series must be over. Restarting from episode 1.")
print("+++++ Not grabbing next episode restarting series, series must be over. Restarting from episode 1.")
first_episode = self.get_first_episode(series)
self.update_shows_table_with_last_episode(series, first_episode[3])
return first_episode
def get_commercial(self, title):
media = "commercials"
sql = "SELECT * FROM "+media+" WHERE (title LIKE ?) COLLATE NOCASE"
self.cursor.execute(sql, (title, ))
datalist = list(self.cursor.fetchone())
if datalist > 0:
print(datalist)
return datalist
else:
return None
return None

View File

@@ -1,17 +1,15 @@
#!/usr/bin/env python
from plexapi.server import PlexServer
from datetime import datetime
import sqlite3
import thread,SocketServer,SimpleHTTPServer
from yattag import Doc
from yattag import indent
import os, sys
import socket
import logging
import logging.handlers
from datetime import datetime
import sqlite3
import thread,SocketServer,SimpleHTTPServer
from plexapi.server import PlexServer
from yattag import Doc
from yattag import indent
class PseudoDailyScheduleController():
@@ -25,32 +23,19 @@ class PseudoDailyScheduleController():
):
self.PLEX = PlexServer(server, token)
self.BASE_URL = server
self.TOKEN = token
self.PLEX_CLIENTS = clients
self.CONTROLLER_SERVER_PATH = controllerServerPath
self.CONTROLLER_SERVER_PORT = controllerServerPort if controllerServerPort != '' else '80'
self.DEBUG = debugMode
self.webserverStarted = False
try:
self.my_logger = logging.getLogger('MyLogger')
self.my_logger.setLevel(logging.DEBUG)
self.handler = logging.handlers.SysLogHandler(address = '/dev/log')
self.my_logger.addHandler(self.handler)
except:
pass
'''
@@ -63,33 +48,22 @@ class PseudoDailyScheduleController():
def get_show_photo(self, section, title):
backgroundImagePath = None
backgroundImgURL = ''
try:
backgroundImagePath = self.PLEX.library.section(section).get(title)
except:
return backgroundImgURL
if backgroundImagePath != None and isinstance(backgroundImagePath.art, str):
backgroundImgURL = self.BASE_URL+backgroundImagePath.art+"?X-Plex-Token="+self.TOKEN
return backgroundImgURL
def start_server(self):
if self.webserverStarted == False and self.CONTROLLER_SERVER_PATH != '':
"""Changing dir to the schedules dir."""
web_dir = os.path.abspath(os.path.join(os.path.dirname( __file__ ), '..', 'schedules'))
os.chdir(web_dir)
PORT = int(self.CONTROLLER_SERVER_PORT)
class MyHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
def log_message(self, format, *args):
@@ -103,46 +77,34 @@ class PseudoDailyScheduleController():
class ReusableTCPServer(SocketServer.TCPServer): allow_reuse_address=True
# specify the httpd service on 0.0.0.0 (all interfaces) on port 80
httpd = ReusableTCPServer(("0.0.0.0", PORT),MyHandler)
# thread this mofo
thread.start_new_thread(httpd.serve_forever,())
# handle keyboard interrupts
except KeyboardInterrupt:
core.print_info("Exiting the SET web server...")
httpd.socket.close()
except socket.error, exc:
print "Caught exception socket.error : %s" % exc
# handle the rest
#except Exception:
# print "[*] Exiting the SET web server...\n"
# httpd.socket.close()
self.webserverStarted = True
def get_xml_from_daily_schedule(self, currentTime, bgImageURL, datalist):
now = datetime.now()
time = now.strftime("%B %d, %Y")
doc, tag, text, line = Doc(
).ttl()
doc.asis('<?xml version="1.0" encoding="UTF-8"?>')
with tag('schedule', currently_playing_bg_image=bgImageURL if bgImageURL != None else ''):
for row in datalist:
if str(row[11]) == "Commercials" and self.DEBUG == False:
continue
timeB = datetime.strptime(row[8], '%I:%M:%S %p')
if currentTime == None:
with tag('time',
('data-key', str(row[12])),
('data-current', 'false'),
@@ -150,11 +112,8 @@ class PseudoDailyScheduleController():
('data-title', str(row[3])),
('data-start-time', str(row[8])),
):
text(row[8])
elif currentTime.hour == timeB.hour and currentTime.minute == timeB.minute:
with tag('time',
('data-key', str(row[12])),
('data-current', 'true'),
@@ -162,11 +121,8 @@ class PseudoDailyScheduleController():
('data-title', str(row[3])),
('data-start-time', str(row[8])),
):
text(row[8])
else:
with tag('time',
('data-key', str(row[12])),
('data-current', 'false'),
@@ -174,12 +130,9 @@ class PseudoDailyScheduleController():
('data-title', str(row[3])),
('data-start-time', str(row[8])),
):
text(row[8])
return indent(doc.getvalue())
'''
*
* Get the generated html for the .html file that is the schedule.
@@ -192,27 +145,18 @@ class PseudoDailyScheduleController():
def get_html_from_daily_schedule(self, currentTime, bgImageURL, datalist):
now = datetime.now()
time = now.strftime("%B %d, %Y")
doc, tag, text, line = Doc(
).ttl()
doc.asis('<!DOCTYPE html>')
with tag('html'):
with tag('head'):
with tag('title'):
text(time + " - Daily Pseudo Schedule")
doc.asis('<link href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.6/css/bootstrap.min.css" rel="stylesheet">')
doc.asis('<link href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.6/css/bootstrap.min.css" rel="stylesheet">')
doc.asis('<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>')
doc.asis("""
<script>
$(function(){
@@ -267,28 +211,17 @@ class PseudoDailyScheduleController():
});
</script>
""")
if bgImageURL != None:
doc.asis('<style>body{ background:transparent!important; } html { background: url('+bgImageURL+') no-repeat center center fixed; -webkit-background-size: cover;-moz-background-size: cover;-o-background-size: cover;background-size: cover;}.make-white { padding: 24px; background:rgba(255,255,255, 0.9); }</style>')
with tag('body'):
with tag('div', klass='container mt-3'):
with tag('div', klass='row make-white'):
with tag('div'):
with tag('div'):
line('h1', "Daily Pseudo Schedule", klass='col-12 pl-0')
with tag('div'):
line('h3', time, klass='col-12 pl-1')
with tag('table', klass='col-12 table table-bordered table-hover'):
with tag('thead', klass='table-info'):
with tag('tr'):
with tag('th'):
@@ -301,23 +234,14 @@ class PseudoDailyScheduleController():
text('Title')
with tag('th'):
text('Start Time')
numberIncrease = 0
for row in datalist:
if str(row[11]) == "Commercials" and self.DEBUG == False:
continue
numberIncrease += 1
with tag('tbody'):
timeB = datetime.strptime(row[8], '%I:%M:%S %p')
if currentTime == None:
with tag('tr'):
with tag('th', scope='row'):
text(numberIncrease)
@@ -329,11 +253,8 @@ class PseudoDailyScheduleController():
text(row[3])
with tag('td'):
text(row[8])
elif currentTime.hour == timeB.hour and currentTime.minute == timeB.minute:
with tag('tr', klass='bg-info'):
with tag('th', scope='row'):
text(numberIncrease)
with tag('td'):
@@ -344,9 +265,7 @@ class PseudoDailyScheduleController():
text(row[3])
with tag('td'):
text(row[8])
else:
with tag('tr'):
with tag('th', scope='row'):
text(numberIncrease)
@@ -358,8 +277,6 @@ class PseudoDailyScheduleController():
text(row[3])
with tag('td'):
text(row[8])
return indent(doc.getvalue())
'''
@@ -372,25 +289,15 @@ class PseudoDailyScheduleController():
def write_schedule_to_file(self, data):
now = datetime.now()
fileName = "index.html"
writepath = './' if os.path.basename(os.getcwd()) == "schedules" else "./schedules/"
if not os.path.exists(writepath):
os.makedirs(writepath)
if os.path.exists(writepath+fileName):
os.remove(writepath+fileName)
mode = 'a' if os.path.exists(writepath) else 'w'
with open(writepath+fileName, mode) as f:
f.write(data)
self.start_server()
'''
@@ -403,26 +310,16 @@ class PseudoDailyScheduleController():
def write_xml_to_file(self, data):
now = datetime.now()
fileName = "pseudo_schedule.xml"
writepath = './' if os.path.basename(os.getcwd()) == "schedules" else "./schedules/"
if not os.path.exists(writepath):
os.makedirs(writepath)
if os.path.exists(writepath+fileName):
os.remove(writepath+fileName)
mode = 'a' if os.path.exists(writepath) else 'w'
with open(writepath+fileName, mode) as f:
f.write(data)
'''
*
* Write 0 or 1 to file for the ajax in the schedule.html to know when to refresh
@@ -433,44 +330,27 @@ class PseudoDailyScheduleController():
def write_refresh_bool_to_file(self):
fileName = "pseudo_refresh.txt"
writepath = './' if os.path.basename(os.getcwd()) == "schedules" else "./schedules/"
first_line = ''
if not os.path.exists(writepath):
os.makedirs(writepath)
if not os.path.exists(writepath+fileName):
file(writepath+fileName, 'w').close()
mode = 'r+'
with open(writepath+fileName, mode) as f:
f.seek(0)
first_line = f.read()
if self.DEBUG:
print "+++++ Html refresh flag: {}".format(first_line)
if first_line == '' or first_line == "0":
f.seek(0)
f.truncate()
f.write("1")
else:
f.seek(0)
f.truncate()
f.write("0")
#f.close()
'''
*
* Trigger "playMedia()" on the Python Plex API for specified media.
@@ -482,63 +362,45 @@ class PseudoDailyScheduleController():
'''
def play_media(self, mediaType, mediaParentTitle, mediaTitle, offset):
try:
if mediaType == "TV Shows":
mediaItems = self.PLEX.library.section(mediaType).get(mediaParentTitle).episodes()
for item in mediaItems:
# print(part.title)
if item.title == mediaTitle:
for client in self.PLEX_CLIENTS:
clientItem = self.PLEX.client(client)
clientItem.playMedia(item, offset=offset)
break
elif mediaType == "Movies":
movie = self.PLEX.library.section(mediaType).get(mediaTitle)
for client in self.PLEX_CLIENTS:
clientItem = self.PLEX.client(client)
clientItem.playMedia(movie, offset=offset)
elif mediaType == "Commercials":
movie = self.PLEX.library.section(mediaType).get(mediaTitle)
for client in self.PLEX_CLIENTS:
clientItem = self.PLEX.client(client)
clientItem.playMedia(movie, offset=offset)
else:
print("##### Not sure how to play {}".format(mediaType))
print "+++++ Done."
except Exception as e:
print e.__doc__
print e.message
print "##### There was an error trying to play the media."
pass
def stop_media(self):
try:
self.my_logger.debug('Trying to stop media.')
for client in self.PLEX_CLIENTS:
clientItem = self.PLEX.client(client)
clientItem.stop(mtype='video')
self.my_logger.debug('Done.')
except Exception as e:
self.my_logger.debug('stop_media - except.', e)
pass
'''
*
* If tv_controller() does not find a "startTime" for scheduled media, search for an "endTime" match for now time.
@@ -550,49 +412,32 @@ class PseudoDailyScheduleController():
def check_for_end_time(self, datalist):
currentTime = datetime.now()
"""c.execute("SELECT * FROM daily_schedule")
datalist = list(c.fetchall())
"""
for row in datalist:
try:
endTime = datetime.strptime(row[9], '%Y-%m-%d %H:%M:%S.%f')
except ValueError:
endTime = datetime.strptime(row[9], '%Y-%m-%d %H:%M:%S')
if currentTime.hour == endTime.hour:
if currentTime.minute == endTime.minute:
if currentTime.second == endTime.second:
if self.DEBUG:
print("Ok end time found")
self.write_schedule_to_file(self.get_html_from_daily_schedule(None, None, datalist))
self.write_xml_to_file(self.get_xml_from_daily_schedule(None, None, datalist))
self.write_refresh_bool_to_file()
break
def play(self, row, datalist, offset=0):
print str("##### Starting Media: '{}'".format(row[3])).encode('UTF-8')
print str("##### Media Offset: '{}' seconds.".format(int(offset / 1000))).encode('UTF-8')
if self.DEBUG:
print str(row).encode('UTF-8')
timeB = datetime.strptime(row[8], '%I:%M:%S %p')
self.play_media(row[11], row[6], row[3], offset)
self.write_schedule_to_file(
self.get_html_from_daily_schedule(
timeB,
@@ -603,9 +448,7 @@ class PseudoDailyScheduleController():
datalist
)
)
self.write_refresh_bool_to_file()
"""Generate / write XML to file
"""
self.write_xml_to_file(
@@ -618,15 +461,11 @@ class PseudoDailyScheduleController():
datalist
)
)
try:
self.my_logger.debug('Trying to play: ' + row[3])
except:
pass
'''
*
* Check DB / current time. If that matches a scheduled shows startTime then trigger play via Plex API
@@ -637,36 +476,21 @@ class PseudoDailyScheduleController():
def tv_controller(self, datalist):
datalistLengthMonitor = 0;
currentTime = datetime.now()
"""c.execute("SELECT * FROM daily_schedule ORDER BY datetime(startTimeUnix) ASC")
datalist = list(c.fetchall())"""
try:
self.my_logger.debug('TV Controller')
except:
pass
for row in datalist:
timeB = datetime.strptime(row[8], '%I:%M:%S %p')
if currentTime.hour == timeB.hour:
if currentTime.minute == timeB.minute:
if currentTime.second == timeB.second:
print("Starting Media: " + row[3])
print(row)
self.play_media(row[11], row[6], row[3])
self.write_schedule_to_file(
self.get_html_from_daily_schedule(
timeB,
@@ -677,9 +501,7 @@ class PseudoDailyScheduleController():
datalist
)
)
self.write_refresh_bool_to_file()
"""Generate / write XML to file
"""
self.write_xml_to_file(
@@ -692,26 +514,18 @@ class PseudoDailyScheduleController():
datalist
)
)
try:
self.my_logger.debug('Trying to play: ' + row[3])
except:
pass
break
datalistLengthMonitor += 1
if datalistLengthMonitor >= len(datalist):
self.check_for_end_time(datalist)
def make_xml_schedule(self, datalist):
print "+++++ ", "Writing XML / HTML to file."
self.write_refresh_bool_to_file()
self.write_schedule_to_file(self.get_html_from_daily_schedule(None, None, datalist))
self.write_xml_to_file(self.get_xml_from_daily_schedule(None, None, datalist))

View File

@@ -3,7 +3,7 @@
# file: startstop.sh
#----
# Simple script to start / stop PseudoChannel.py
# Simple script to start / stop a python script in the background.
#----
#----
@@ -13,34 +13,79 @@
#----BEGIN EDITABLE VARS----
pid_file=running.pid
SCRIPT_TO_EXECUTE_PLUS_ARGS='PseudoChannel.py -m -r'
output_pid_path=.
OUTPUT_PID_FILE=running.pid
python_to_use="$(which python)"
OUTPUT_PID_PATH=.
PYTHON_TO_USE="$(which python)"
# If using 'virtualenv' with python, specify the local virtualenv dir.
VIRTUAL_ENV_DIR="env"
#----END EDITABLE VARS-------
if [ ! -e $output_pid_path/$pid_file ]; then
# If virtualenv specified & exists, using that version of python instead.
if [ -d "$VIRTUAL_ENV_DIR" ]; then
PYTHON_TO_USE="$VIRTUAL_ENV_DIR/bin/python"
fi
# If the .pid file doesn't exist (let's assume no processes are running)...
if [ ! -e "$OUTPUT_PID_PATH/$OUTPUT_PID_FILE" ]; then
# If the running.pid file doesn't exists, create it, start PseudoChannel.py and add the PID to it.
nohup $python_to_use ./PseudoChannel.py -m -r > /dev/null 2>&1 & echo $! > $output_pid_path/$pid_file
"$PYTHON_TO_USE" ./$SCRIPT_TO_EXECUTE_PLUS_ARGS > /dev/null 2>&1 & echo $! > "$OUTPUT_PID_PATH/$OUTPUT_PID_FILE"
echo "Started PseudoChannel.py @ Process: $!"
echo "Created $pid_file file in $output_pid_path dir"
echo "Started $SCRIPT_TO_EXECUTE_PLUS_ARGS @ Process: $!"
sleep .7
echo "Created $OUTPUT_PID_FILE file in $OUTPUT_PID_PATH dir"
else
# If the running.pid exists, read it & try to kill the process if it exists, then delete it.
the_pid=$(<$output_pid_path/$pid_file)
rm $output_pid_path/$pid_file
echo "Deleted $pid_file file in $output_pid_path dir"
kill $the_pid
while [ -e /proc/$the_pid ]
do
echo "PseudoChannel.py @: $the_pid is still running"
sleep .6
done
echo "PseudoChannel.py @: $the_pid has finished"
the_pid=$(<$OUTPUT_PID_PATH/$OUTPUT_PID_FILE)
fi
rm "$OUTPUT_PID_PATH/$OUTPUT_PID_FILE"
echo "Deleted $OUTPUT_PID_FILE file in $OUTPUT_PID_PATH dir"
echo "kill: $the_pid"
kill "$the_pid"
COUNTER=1
while [ -e /proc/$the_pid ]
do
echo "$SCRIPT_TO_EXECUTE_PLUS_ARGS @: $the_pid is still running"
sleep .7
COUNTER=$[$COUNTER +1]
if [ $COUNTER -eq 20 ]; then
kill -9 "$the_pid"
fi
if [ $COUNTER -eq 40 ]; then
exit 1
fi
done
echo "$SCRIPT_TO_EXECUTE_PLUS_ARGS @: $the_pid has finished"
fi
exit 0

0
tests/__init__.py Normal file
View File

View File

@@ -0,0 +1,17 @@
import pytest
import datetime
@pytest.mark.parametrize("commercial, expected", [
(["1", "1501900754", "3", "001 - Kit_Kat_Commercial_-_Give_Me_A_Break_1988", "30890", "/library/metadata/3854"], 35890)
])
def test_pad_the_commercial_dur(commercial, expected):
commercial_as_list = list(commercial)
commercial_as_list[4] = int(commercial_as_list[4]) + (5 * 1000)
assert int(commercial_as_list[4]) == expected
def test_inject_commercials():
pass

View File

@@ -0,0 +1,25 @@
import pytest
import datetime
@pytest.mark.parametrize("prevstartime, prevendtime, nowtime, expected", [
("04:55:00 PM", "1900-01-01 17:23:42.304000", "1900-01-01 17:10:42.304000", True),
("04:55:00 PM", "1900-01-01 17:23:42.304000", "1900-01-01 16:56:42.304000", True),
("04:55:00 PM", "1900-01-01 17:23:42.304000", "1900-01-01 16:59:42.304000", True),
("04:55:00 PM", "1900-01-01 17:23:42.304000", "1900-01-01 17:02:42.304000", True),
("04:55:00 PM", "1900-01-01 17:23:42.304000", "1900-01-01 17:15:42.304000", True),
("04:55:00 PM", "1900-01-01 17:23:42.304000", "1900-01-01 17:23:43.304000", False),
("04:55:00 PM", "1900-01-01 17:23:42.304000", "1900-01-01 17:25:00.304000", False),
])
def test_prev_day_media_still_playing_on_update(prevendtime, prevstartime, nowtime, expected):
prev_end_time_to_watch_for = None
now = datetime.datetime.strptime(nowtime, '%Y-%m-%d %H:%M:%S.%f')
prev_start_time = datetime.datetime.strptime(prevstartime, "%I:%M:%S %p")
prev_end_time_format = '%Y-%m-%d %H:%M:%S.%f' if '.' in prevendtime else '%Y-%m-%d %H:%M:%S'
prev_end_time = datetime.datetime.strptime(prevendtime, prev_end_time_format)
assert (prev_start_time < now and prev_end_time > now) == expected