1
0
mirror of https://github.com/fHDHR/fHDHR_NextPVR.git synced 2025-12-06 09:26:57 -05:00

Compare commits

...

186 Commits

Author SHA1 Message Date
Deathbybandaid
e40543552a
Merge pull request #160 from deathbybandaid/dev
Update Readme.md with Deprecation Warning
2021-02-01 09:39:09 -05:00
deathbybandaid
c376cfcaa9 Update Readme.md with Deprecation Warning 2021-02-01 09:37:28 -05:00
Deathbybandaid
6ecf54a8d0
Merge pull request #159 from deathbybandaid/dev
Improve Config for Zero values
2021-02-01 08:03:39 -05:00
deathbybandaid
0d185e6704 Improve Config for Zero values 2021-02-01 08:01:52 -05:00
Deathbybandaid
09ddda62fc
Merge pull request #158 from deathbybandaid/dev
Channel Editor Web Fixings
2021-01-31 16:55:02 -05:00
deathbybandaid
b1d4e13c31 Channel Editor Web Fixings 2021-01-31 16:54:07 -05:00
Deathbybandaid
eaf26457f0
Merge pull request #157 from deathbybandaid/dev
Fix Handling of missing Versions for ffmpeg/vlc
2021-01-31 16:00:50 -05:00
deathbybandaid
b92247c810 Fix Handling of missing Versions for ffmpeg/vlc 2021-01-31 15:59:33 -05:00
Deathbybandaid
0fb454016d
Merge pull request #156 from deathbybandaid/dev
Dev
2021-01-31 15:31:01 -05:00
deathbybandaid
ab51ea02a1 Improve Detection of ffmpeg/vlc application paths 2021-01-31 15:24:19 -05:00
deathbybandaid
884d4b6e27 Improve Tuner Methods per Origin 2021-01-31 14:21:54 -05:00
Deathbybandaid
9c72f30a99
Merge pull request #155 from deathbybandaid/dev
Repair Tuner Scanning
2021-01-30 11:42:54 -05:00
deathbybandaid
72627510aa Repair Tuner Scanning 2021-01-30 11:41:57 -05:00
Deathbybandaid
9caeac2f41
Merge pull request #154 from deathbybandaid/dev
Dev
2021-01-30 11:11:36 -05:00
deathbybandaid
d03e575f0b Repair m3u/w3u 2021-01-30 11:10:16 -05:00
deathbybandaid
54e1e72104 Repair Stream Plugins 2021-01-30 11:00:58 -05:00
Deathbybandaid
b16fcf3b51
Merge pull request #153 from deathbybandaid/dev
Bugfixes
2021-01-30 10:42:48 -05:00
deathbybandaid
e86290e9fe Bugfixes 2021-01-30 10:41:46 -05:00
Deathbybandaid
0de184c242
Merge pull request #152 from deathbybandaid/dev
repair hdhr
2021-01-29 16:22:20 -05:00
deathbybandaid
5f4092bdc8 repair hdhr 2021-01-29 16:21:02 -05:00
Deathbybandaid
d9cda8b1d4
Merge pull request #151 from deathbybandaid/dev
Rebuild Plugin System and seperate core functionality into Plugins
2021-01-29 15:37:45 -05:00
deathbybandaid
a7c854bcd4 Rebuild Plugin System and seperate core functionality into Plugins 2021-01-29 15:29:05 -05:00
Deathbybandaid
13faf0845e
Merge pull request #150 from deathbybandaid/dev
Strip invalid Config Options
2021-01-23 18:52:16 -05:00
deathbybandaid
1cf2a7acce Strip invalid Config Options 2021-01-23 18:50:51 -05:00
Deathbybandaid
36712e7ba0
Merge pull request #149 from deathbybandaid/dev
Update CSS styling
2021-01-23 18:31:48 -05:00
deathbybandaid
02e825978b Update CSS styling 2021-01-23 18:30:24 -05:00
Deathbybandaid
9642feecae
Merge pull request #148 from deathbybandaid/dev
Make alt_stream methods into plugins and enhance Config system
2021-01-23 17:59:41 -05:00
deathbybandaid
b8ce4f4e8a Make alt_stream methods into plugins and enhance Config system 2021-01-23 17:50:14 -05:00
Deathbybandaid
22955ce11f
Merge pull request #147 from deathbybandaid/dev
Implement Plugin based Installation
2021-01-22 15:49:38 -05:00
deathbybandaid
1aa35b66f0 Implement Plugin based Installation 2021-01-22 15:35:04 -05:00
Deathbybandaid
cbe8deb965
Merge pull request #146 from deathbybandaid/dev
Improve Channel Creation Logic
2021-01-22 11:40:33 -05:00
deathbybandaid
d118ef7807 Improve Channel Creation Logic 2021-01-22 11:38:35 -05:00
Deathbybandaid
c9b20743fd
Merge pull request #145 from deathbybandaid/dev
Improve future modularity
2021-01-22 08:52:33 -05:00
deathbybandaid
1b13aedc5e Improve future modularity 2021-01-22 08:49:16 -05:00
Deathbybandaid
ddcb04892b
Merge pull request #144 from deathbybandaid/dev
Make alt_epg loading more abstract
2021-01-21 15:29:44 -05:00
deathbybandaid
6076011d1c Make alt_epg loading more abstract 2021-01-21 15:20:28 -05:00
Deathbybandaid
751eaebee9
Merge pull request #143 from deathbybandaid/dev
Update Streaming Config Docs
2021-01-21 12:23:49 -05:00
deathbybandaid
53dc0e127d Update Streaming Config Docs 2021-01-21 12:17:39 -05:00
Deathbybandaid
acf72ad109
Merge pull request #142 from deathbybandaid/dev
Implement Transcode Profiles for ffmpeg/vlc
2021-01-21 11:49:23 -05:00
deathbybandaid
630b8dbf2b Implement Transcode Profiles for ffmpeg/vlc 2021-01-21 11:46:44 -05:00
Deathbybandaid
e8aa5bd3f4
Merge pull request #141 from deathbybandaid/dev
Dev
2021-01-21 10:24:12 -05:00
deathbybandaid
03927ec495 Logging that Direct Method cannot transcode 2021-01-21 09:43:33 -05:00
deathbybandaid
6a924cbca2 Allow ffmpeg/vlc to select quality when origin_quality is set to None 2021-01-21 09:33:26 -05:00
Deathbybandaid
4bd2ff971e
Merge pull request #140 from deathbybandaid/dev
Improve Streams with Headers and Quality Options
2021-01-20 16:09:51 -05:00
deathbybandaid
63685f4a0e Improve Streams with Headers and Quality Options 2021-01-20 16:01:32 -05:00
Deathbybandaid
70fe2f3814
Merge pull request #139 from deathbybandaid/dev
Repair web Logging and Add Version Patch Number
2021-01-20 12:27:40 -05:00
deathbybandaid
c0ff51a6db Repair web Logging and Add Version Patch Number 2021-01-20 12:24:44 -05:00
Deathbybandaid
8a3f8d919d
Merge pull request #138 from deathbybandaid/dev
Add EPG API Method
2021-01-20 11:25:01 -05:00
deathbybandaid
e09bb5d83b Add EPG API Method 2021-01-20 11:21:58 -05:00
Deathbybandaid
b72a26c96c
Merge pull request #137 from deathbybandaid/dev
Update Docs and move Stream Method Setting
2021-01-20 11:01:49 -05:00
deathbybandaid
35e6bb707d Update Docs and move Stream Method Setting 2021-01-20 10:53:20 -05:00
Deathbybandaid
fc0708d888
Merge pull request #136 from deathbybandaid/dev
Move Logger
2021-01-18 09:03:48 -05:00
deathbybandaid
9d25b63f99 Move Logger 2021-01-18 08:54:21 -05:00
Deathbybandaid
af9fbe97b9
Merge pull request #135 from deathbybandaid/dev
Catch last channel entries on page
2021-01-17 11:42:26 -05:00
deathbybandaid
a36b9e8143 Catch last channel entries on page 2021-01-17 11:40:35 -05:00
Deathbybandaid
ef82a3b5e7
Merge pull request #134 from deathbybandaid/dev
Enable EPG Config Settings
2021-01-16 09:02:12 -05:00
deathbybandaid
6bf4319316 Enable EPG Config Settings 2021-01-16 08:59:33 -05:00
Deathbybandaid
9ad3f00482
Merge pull request #133 from deathbybandaid/dev
Repair Tuner Page Table
2021-01-15 08:54:45 -05:00
deathbybandaid
31f7213006 Repair Tuner Page Table 2021-01-15 08:41:43 -05:00
Deathbybandaid
ccd99a7008
Merge pull request #132 from deathbybandaid/dev
Enhance Channel Editor
2021-01-14 09:36:22 -05:00
deathbybandaid
56d7a74ee4 Enhance Channel Editor 2021-01-14 09:33:36 -05:00
Deathbybandaid
e3d8f64c5c
Merge pull request #131 from deathbybandaid/dev
Make Universal Brython Functions
2021-01-13 11:15:35 -05:00
deathbybandaid
28383d89ec Make Universal Brython Functions 2021-01-13 11:12:20 -05:00
Deathbybandaid
d915e4cbed
Merge pull request #130 from deathbybandaid/dev
Repair Tab/Space issues in Templates
2021-01-13 10:15:20 -05:00
deathbybandaid
51b9a85597 Repair Tab/Space issues in Templates 2021-01-13 10:10:10 -05:00
Deathbybandaid
051fb87add
Merge pull request #129 from deathbybandaid/dev
Move API Notifications
2021-01-12 16:24:44 -05:00
deathbybandaid
796a5e9eec Move API Notifications 2021-01-12 16:21:32 -05:00
Deathbybandaid
7348101eea
Merge pull request #128 from deathbybandaid/dev
Dev
2021-01-12 12:05:21 -05:00
deathbybandaid
fb1f5f2324 use session id as stream client id 2021-01-12 12:03:17 -05:00
deathbybandaid
887b60b80d Implement w3u standard 2021-01-12 11:56:17 -05:00
Deathbybandaid
698e407c38
Merge pull request #127 from deathbybandaid/dev
Add Additional Channel Creation Handling
2021-01-11 12:57:10 -05:00
deathbybandaid
8c80c51c2a Add Additional Channel Creation Handling 2021-01-11 12:53:22 -05:00
Deathbybandaid
1eca980ae5
Merge pull request #126 from deathbybandaid/dev
Add Additional Error Handling for Streams
2021-01-11 10:19:10 -05:00
deathbybandaid
308c56da09 Add Additional Error Handling for Streams 2021-01-11 10:16:11 -05:00
Deathbybandaid
404f44c22d
Merge pull request #125 from deathbybandaid/dev
More String Formatting
2021-01-11 09:24:51 -05:00
deathbybandaid
cd24c5a4fe More String Formatting 2021-01-11 08:58:48 -05:00
Deathbybandaid
db49d28de5
Merge pull request #124 from deathbybandaid/dev
Reduce CPU Usage
2021-01-09 15:52:22 -05:00
deathbybadaid
1f23425be5 Reduce CPU Usage 2021-01-09 15:44:59 -05:00
Deathbybandaid
4efdded7e1
Merge pull request #123 from deathbybandaid/dev
Update String Formatting for Alternative_EPG
2021-01-08 15:58:48 -05:00
deathbybadaid
b4d8ed6e4d Update String Formatting for Alternative_EPG 2021-01-08 15:57:50 -05:00
Deathbybandaid
cdbe545df6
Merge pull request #122 from deathbybandaid/dev
Update String Formatting
2021-01-08 15:38:25 -05:00
deathbybadaid
d1038ab46a Update String Formatting 2021-01-08 15:35:55 -05:00
Deathbybandaid
8f9208c2cb
Merge pull request #121 from deathbybandaid/dev
Update Origin_web
2021-01-08 12:38:09 -05:00
deathbybadaid
93e07fd771 Update Origin_web 2021-01-08 12:35:18 -05:00
Deathbybandaid
f5967e718a
Merge pull request #120 from deathbybandaid/dev
Enhance M3U and Stream URLs
2021-01-08 11:13:57 -05:00
deathbybadaid
32252e34c9 Enhance M3U and Stream URLs 2021-01-08 11:11:43 -05:00
Deathbybandaid
7e2accd2d2
Merge pull request #119 from deathbybandaid/dev
Enhance Diagnostics Page
2021-01-08 10:29:38 -05:00
deathbybadaid
244472792e Enhance Diagnostics Page 2021-01-08 10:25:35 -05:00
Deathbybandaid
418b23e96b
Merge pull request #118 from deathbybandaid/dev
Web Enhancements
2021-01-08 09:36:47 -05:00
deathbybadaid
d9e0cc13dd Web Enhancements 2021-01-08 09:29:15 -05:00
Deathbybandaid
c444d3123c
Merge pull request #117 from deathbybandaid/dev
Add missing comma to m3u
2021-01-07 17:28:26 -05:00
deathbybandaid
e003d502c2 Add missing comma to m3u 2021-01-07 17:25:47 -05:00
Deathbybandaid
a1e9e28e64
Merge pull request #116 from deathbybandaid/dev
Implement access_levels and route_list
2021-01-07 15:00:32 -05:00
deathbybandaid
4093a8c135 Implement access_levels and route_list 2021-01-07 14:56:02 -05:00
Deathbybandaid
e7282522b5
Merge pull request #115 from deathbybandaid/dev
Add Restart Button to Web Interface
2021-01-07 11:56:06 -05:00
deathbybandaid
602e74f565 Add Restart Button to Web Interface 2021-01-07 11:54:02 -05:00
Deathbybandaid
46a6043e62
Merge pull request #114 from deathbybandaid/dev
Repair Tuner Channel Scan URL
2021-01-07 09:35:39 -05:00
deathbybandaid
d87ef97494 Repair Tuner Channel Scan URL 2021-01-07 09:32:52 -05:00
Deathbybandaid
038bd03b42
Merge pull request #113 from deathbybandaid/dev
Dev
2021-01-07 08:49:58 -05:00
deathbybandaid
cd47fa0a3f Add Better EPG Logging 2021-01-07 08:40:25 -05:00
deathbybandaid
cf64aecf7b Streaming Enhancements 2021-01-06 14:25:45 -05:00
Deathbybandaid
b6ef8b13ae
Merge pull request #112 from deathbybandaid/dev
Update Version Numbers for Next Release
2021-01-06 09:51:39 -05:00
deathbybandaid
90fb90a92e Update Version Numbers for Next Release 2021-01-06 09:49:21 -05:00
Deathbybandaid
0441e731d4
Merge pull request #111 from deathbybandaid/dev
Missing __init__.py
2021-01-06 09:14:39 -05:00
deathbybandaid
e87af73215 Missing __init__.py 2021-01-06 09:06:48 -05:00
Deathbybandaid
f1dab94210
Merge pull request #110 from deathbybandaid/dev
Patch EPG timeout Error
2021-01-06 09:00:45 -05:00
deathbybadaid
75a8492dbe Patch EPG timeout Error 2021-01-06 08:58:00 -05:00
Deathbybandaid
c7c5efdc4e
Merge pull request #109 from DanAustinGH/dev-ui
Core UI changes
2021-01-06 08:18:00 -05:00
DanAustinGH
51d9728d2a Core UI changes 2021-01-05 16:26:10 -07:00
Deathbybandaid
27a8045fc1
Merge pull request #108 from deathbybandaid/dev
Improve Internal API System
2021-01-05 14:07:22 -05:00
deathbybandaid
8a475f154a Improve Internal API System 2021-01-05 14:01:19 -05:00
Deathbybandaid
64a6e4a635
Merge pull request #107 from deathbybandaid/dev
Dev
2021-01-05 12:05:26 -05:00
deathbybadaid
e9e0e40d78 Fix Streaming 2021-01-05 12:04:41 -05:00
deathbybadaid
c0acaa736e Bugfix 2021-01-05 11:49:01 -05:00
Deathbybandaid
f733b8c9df
Merge pull request #106 from deathbybandaid/dev
Strip Multiprocessing and Re-add Windows Support
2021-01-05 11:33:09 -05:00
deathbybadaid
3f314f3863 Strip Multiprocessing and Re-add Windows Support 2021-01-05 11:28:40 -05:00
Deathbybandaid
0c09e1dca0
Merge pull request #105 from deathbybandaid/dev
Dev
2021-01-05 11:08:37 -05:00
deathbybadaid
5c66f2594e Improve Tuner Grabbing and closing 2021-01-05 11:06:14 -05:00
deathbybadaid
1199489cc3 Fix EPG Update Typos 2021-01-05 10:55:56 -05:00
Deathbybandaid
1ab992fa76
Merge pull request #104 from deathbybandaid/dev
Add Detection of Internal API calls
2021-01-05 09:51:41 -05:00
deathbybadaid
2cf0f3d891 Add Detection of Internal API calls 2021-01-05 09:47:34 -05:00
Deathbybandaid
fb44ee3bde
Merge pull request #103 from deathbybandaid/dev
Update Stream Methods
2021-01-04 10:21:58 -05:00
deathbybandaid
e7e4ddcade Update Stream Methods 2021-01-04 10:18:52 -05:00
Deathbybandaid
dace8fa650
Merge pull request #102 from deathbybandaid/dev
SSDP Enhancements
2021-01-02 17:41:02 -05:00
deathbybandaid
8fc69ba973 SSDP Enhancements 2021-01-02 17:27:31 -05:00
Deathbybandaid
87633c356e
Merge pull request #101 from deathbybandaid/dev
Patch User-Agent detection
2021-01-02 15:53:13 -05:00
deathbybandaid
341d905ea2 Patch User-Agent detection 2021-01-02 15:44:31 -05:00
Deathbybandaid
c05ae6ac71
Merge pull request #100 from deathbybandaid/dev
Fix Lineup Tab issue
2021-01-02 15:03:41 -05:00
deathbybandaid
aec09eade1 Fix Lineup Tab issue 2021-01-02 15:00:12 -05:00
Deathbybandaid
28a9886960
Merge pull request #99 from deathbybandaid/dev
Improve Channel Sorting Globally
2021-01-02 12:36:26 -05:00
deathbybandaid
adae4d77c7 Improve Channel Sorting Globally 2021-01-02 12:33:11 -05:00
Deathbybandaid
cebce2f1ba
Merge pull request #98 from deathbybandaid/dev
Dev
2021-01-01 18:25:55 -05:00
deathbybandaid
2cf4f4249b Fix Blocks EPG 2021-01-01 18:22:41 -05:00
deathbybandaid
fa6e3bdd50 Fix EPG 2021-01-01 18:18:23 -05:00
Deathbybandaid
a981d8d845
Merge pull request #97 from deathbybandaid/dev
Enhance Channel Loading
2021-01-01 18:08:49 -05:00
deathbybandaid
39649a11f8 Enhance Channel Loading 2021-01-01 18:05:36 -05:00
Deathbybandaid
2858016ad7
Merge pull request #96 from deathbybandaid/dev
Ensure database values are preserved when loaded into the UI.
2020-12-29 13:41:56 -05:00
deathbybandaid
77f941c08a Ensure database values are preserved when loaded into the UI. 2020-12-29 13:38:43 -05:00
Deathbybandaid
9f37dfa6b3
Merge pull request #95 from deathbybandaid/dev
Dev
2020-12-20 11:29:35 -05:00
deathbybandaid
e3a264f24e Use session to store the DeviceAuth 2020-12-20 11:22:46 -05:00
deathbybandaid
2cd30a38ca Use session to detect if PlexMediaServer 2020-12-20 11:13:00 -05:00
deathbybandaid
b12996b8bb Create Flask Session system and mobile device detection 2020-12-20 10:52:51 -05:00
deathbybandaid
7e4eea1d76 Cleanup obsolete code from first webUI iteration 2020-12-20 10:26:20 -05:00
Deathbybandaid
b8be38db68
Merge pull request #94 from deathbybandaid/dev
Add Mass Scale Channel Edit API Method
2020-12-18 10:29:21 -05:00
deathbybandaid
5b1c9b303b Add Mass Scale Channel Edit API Method 2020-12-18 10:08:19 -05:00
Deathbybandaid
0b8a5104a9
Merge pull request #93 from deathbybandaid/dev
Dev
2020-12-18 09:02:18 -05:00
deathbybandaid
0aea878ebe More EPG Enhancements 2020-12-18 08:54:07 -05:00
deathbybandaid
86c14c2d9b Reformat xmltv Date 2020-12-18 07:59:39 -05:00
Deathbybandaid
eaf5e89113
Merge pull request #92 from deathbybandaid/dev
Dev
2020-12-17 14:12:24 -05:00
deathbybandaid
ca96d4de34 Add More Thumbnail Handling 2020-12-17 14:06:36 -05:00
deathbybandaid
9c481d4103 Add Hidden Tools Page 2020-12-16 16:02:25 -05:00
Deathbybandaid
1d595d8261
Merge pull request #91 from deathbybandaid/dev
Implement internal client system
2020-12-16 10:40:39 -05:00
deathbybandaid
845efb0719 Implement internal client system 2020-12-16 09:42:12 -05:00
Deathbybandaid
91926a2dcf
Merge pull request #90 from deathbybandaid/dev
Dev
2020-12-16 09:38:21 -05:00
deathbybandaid
0cc13306ab Sleep to make sure Flask is ready for startup tasks 2020-12-16 08:39:42 -05:00
deathbybandaid
d47ecee009 EPG Update Bugfix 2020-12-16 08:25:46 -05:00
Deathbybandaid
60455b6a84
Merge pull request #89 from deathbybandaid/dev
Dev
2020-12-15 13:35:18 -05:00
deathbybandaid
886b257228 Add Tuner API Status Method 2020-12-15 13:30:59 -05:00
deathbybandaid
3f8ff15e97 Move Startup Tasks to API 2020-12-15 12:59:53 -05:00
Deathbybandaid
baf4cf461c
Merge pull request #88 from deathbybandaid/dev
Dev
2020-12-15 11:50:05 -05:00
deathbybandaid
73c5f23ed7 Add Tweak for future UDP variants 2020-12-15 11:43:34 -05:00
deathbybandaid
61779991ca Change Default Logging Level to INFO 2020-12-15 11:33:51 -05:00
deathbybandaid
1143b158da Add Channel Import Logging 2020-12-15 11:27:41 -05:00
Deathbybandaid
40971388ce
Merge pull request #87 from deathbybandaid/dev
Dev
2020-12-14 13:49:26 -05:00
deathbybandaid
7f6d80fd3e EPG Enhancements 2020-12-14 11:03:45 -05:00
deathbybandaid
55ca03b389 Improve Missing Channel EPG System 2020-12-10 16:01:49 -05:00
Deathbybandaid
d101717e8f
Merge pull request #86 from deathbybandaid/dev
Dev
2020-12-10 15:18:52 -05:00
deathbybandaid
a8972371c2 Enhance Channel Numbering system 2020-12-10 14:27:08 -05:00
deathbybandaid
d5c7a1ea47 Allow Cluster Bar Hiding 2020-12-10 12:19:56 -05:00
deathbybandaid
afed209051 Add Advanced Button 2020-12-10 12:06:24 -05:00
deathbybandaid
328320192a Move Web Conf 2020-12-10 11:59:21 -05:00
deathbybandaid
71da17fa45 Enhance Config Variable Validation 2020-12-10 11:35:26 -05:00
Deathbybandaid
295a7259ac
Merge pull request #85 from deathbybandaid/dev
Dev
2020-12-10 10:29:27 -05:00
deathbybandaid
e00cec1ed6 Move Origin Web Page and Create API framework 2020-12-10 09:41:45 -05:00
deathbybandaid
8b870430ad Update EPG Directory Structure 2020-12-09 16:26:43 -05:00
deathbybandaid
01c7b7fd99 Enable tvtv 2020-12-09 16:01:59 -05:00
deathbybandaid
6786f10812 Add tvtv EPG method 2020-12-09 16:00:14 -05:00
deathbybandaid
5ff0cdf93c Bugfix for Origin EPG 2020-12-09 15:22:33 -05:00
deathbybandaid
105e1a34a5 Update Zap2it 2020-12-09 15:16:18 -05:00
deathbybandaid
eb55403bee EPG/XMLTV Tweaks 2020-12-09 15:11:39 -05:00
Deathbybandaid
142b9fd3ab
Merge pull request #84 from deathbybandaid/dev
Bugfix
2020-12-09 12:16:29 -05:00
deathbybandaid
bfdbeded3a Bugfix 2020-12-09 12:13:01 -05:00
Deathbybandaid
295599ac55
Merge pull request #83 from deathbybandaid/dev
Update Conf
2020-12-08 15:36:07 -05:00
deathbybandaid
b75d4284d0 Update Conf 2020-12-08 15:16:35 -05:00
Deathbybandaid
f9da9d8a72
Merge pull request #82 from deathbybandaid/dev
Update Guide Page with Conditional Play Link
2020-12-08 14:55:44 -05:00
deathbybandaid
10311a767e Update Guide Page with Conditional Play Link 2020-12-08 14:53:45 -05:00
173 changed files with 20013 additions and 3305 deletions

View File

@ -16,5 +16,7 @@ fHDHR is labeled as beta until we reach v1.0.0
Join us in `#fHDHR <irc://irc.freenode.net/#fHDHR>`_ on Freenode. Join us in `#fHDHR <irc://irc.freenode.net/#fHDHR>`_ on Freenode.
# !!NOTICE!!
Due to multiple issues, I'm dropping official support for Windows. To reduce code duplication between variants, I am moving to a plugin system.
The normal variant repos will stay active during the transition.

View File

@ -1,2 +0,0 @@
# pylama:ignore=W0401,W0611
from .zap2it import *

View File

@ -1,62 +0,0 @@
[main]
# uuid =
# cache_dir =
# servicename = NextPVR
# reponame = fHDHR_NextPVR
[fhdhr]
# address = 0.0.0.0
# discovery_address = 0.0.0.0
# port = 5004
# stream_type = direct
# tuner_count = 4
# friendlyname = fHDHR-NextPVR
# reporting_firmware_name = fHDHR_NextPVR
# reporting_manufacturer = BoronDust
# reporting_model = fHDHR
# reporting_firmware_ver = 20201001
# reporting_tuner_type = Antenna
# device_auth = fHDHR
[epg]
# images = pass
# method = origin
# update_frequency = 43200
[ffmpeg]
# path = ffmpeg
# bytes_per_read = 1152000
[vlc]
# path = cvlc
# bytes_per_read = 1152000
[direct_stream]
# chunksize = 1048576
[logging]
# level = WARNING
[database]
# type = sqlite
# driver = None
[nextpvr]
# address = localhost
# port = 8866
# ssl =
# pin =
[zap2it]
# delay = 5
# postalcode = None
# affiliate_id = gapzap
# country = USA
# device = -
# headendid = lineupId
# isoverride = True
# languagecode = en
# pref =
# timespan = 6
# timezone =
# userid = -

View File

@ -6,12 +6,33 @@
"config_web": true "config_web": true
}, },
"method":{ "method":{
"value": "blocks", "value": "none",
"config_file": true, "config_file": true,
"config_web": true "config_web": true
}, },
"update_frequency":{ "update_frequency":{
"value": 14400, "value": 43200,
"config_file": true,
"config_web": true
},
"reverse_days": {
"value": -1,
"config_file": true,
"config_web": true
},
"forward_days": {
"value": 7,
"config_file": true,
"config_web": true
},
"block_size": {
"value": 1800,
"config_file": true,
"config_web": true
}
,
"xmltv_offset": {
"value": "+0000",
"config_file": true, "config_file": true,
"config_web": true "config_web": true
} }

View File

@ -15,26 +15,6 @@
"config_file": true, "config_file": true,
"config_web": true "config_web": true
}, },
"reporting_manufacturer":{
"value": "BoronDust",
"config_file": true,
"config_web": true
},
"reporting_model":{
"value": "fHDHR",
"config_file": true,
"config_web": true
},
"reporting_firmware_ver":{
"value": "20201001",
"config_file": true,
"config_web": true
},
"reporting_tuner_type":{
"value": "Antenna",
"config_file": true,
"config_web": true
},
"device_auth":{ "device_auth":{
"value": "fHDHR", "value": "fHDHR",
"config_file": true, "config_file": true,
@ -54,21 +34,6 @@
"value": "fHDHR", "value": "fHDHR",
"config_file": true, "config_file": true,
"config_web": true "config_web": true
},
"stream_type":{
"value": "direct",
"config_file": true,
"config_web": true
},
"tuner_count":{
"value": 4,
"config_file": true,
"config_web": true
},
"reporting_firmware_name":{
"value": "fHDHR",
"config_file": true,
"config_web": true
} }
} }
} }

View File

@ -1,7 +1,7 @@
{ {
"logging":{ "logging":{
"level":{ "level":{
"value": "WARNING", "value": "INFO",
"config_file": true, "config_file": true,
"config_web": true "config_web": true
} }

View File

@ -10,35 +10,15 @@
"config_file": true, "config_file": true,
"config_web": true "config_web": true
}, },
"thread_method":{
"value": "multiprocessing",
"config_file": true,
"config_web": true
},
"servicename":{ "servicename":{
"value": "fHDHR", "value": "fHDHR",
"config_file": false, "config_file": false,
"config_web": false "config_web": false
}, },
"dictpopname":{
"value": "fHDHR",
"config_file": false,
"config_web": false
},
"reponame":{ "reponame":{
"value": "fHDHR", "value": "fHDHR",
"config_file": false, "config_file": false,
"config_web": false "config_web": false
},
"valid_epg_methods":{
"value": "None,blocks",
"config_file": false,
"config_web": false
},
"required":{
"value": "none",
"config_file": false,
"config_web": false
} }
} }
} }

View File

@ -1,9 +0,0 @@
{
"rmg":{
"enabled":{
"value": true,
"config_file": true,
"config_web": false
}
}
}

View File

@ -1,31 +1,22 @@
{ {
"ffmpeg":{ "streaming":{
"path":{
"value": "ffmpeg",
"config_file": true,
"config_web": true
},
"bytes_per_read": { "bytes_per_read": {
"value": 1152000, "value": 1152000,
"config_file": true, "config_file": true,
"config_web": true "config_web": true
}
}, },
"vlc":{ "origin_quality": {
"path":{ "value": "none",
"value": "cvlc",
"config_file": true, "config_file": true,
"config_web": true "config_web": true
}, },
"bytes_per_read":{ "transcode_quality": {
"value": 1152000, "value": "none",
"config_file": true, "config_file": true,
"config_web": true "config_web": true
}
}, },
"direct_stream":{ "method": {
"chunksize":{ "value": "direct",
"value": 1048576,
"config_file": true, "config_file": true,
"config_web": true "config_web": true
} }

View File

@ -28,14 +28,33 @@ Here's the `main` section.
# cache_dir = # cache_dir =
```` ````
## streaming
* `method` can be set to `ffmpeg`, `vlc` or `direct`.
* `bytes_per_read` determines how many bytes of the stream to read before sending the data to your client. Increasing this value may cause longer load times, and lowering it may effect `stuttering`.
* `origin_quality` can be set to high,medium,low for most variants. Variants that make use of m3u8 will Autoselect High for the direct method if not set. ffmpeg/vlc will determine the best stream on their own. Some Variants can allow alternative values.
* `transcode_quality` works with ffmpeg/vlc to use fHDHR for handling quality instead of the origin. Valid settings include: heavy,mobile,internet720,internet480,internet360,internet240
````
[streaming]
# method = direct
# bytes_per_read = 1152000
# origin_quality = None
# transcode_quality = None
````
## fhdhr ## fhdhr
The `fhdhr` contains all the configuration options for interfacing between this script and your media platform. The `fhdhr` contains all the configuration options for interfacing between this script and your media platform.
* `address` and `port` are what we will allow the script to listen on. `0.0.0.0` is the default, and will respond to all. * `address` and `port` are what we will allow the script to listen on. `0.0.0.0` is the default, and will respond to all.
* `discovery_address` may be helpful for making SSDP work properly. If `address` is not `0.0.0.0`, we will use that. If this is not set to a real IP, we won't run SSDP. SSDP is only really helpful for discovering in Plex/Emby. It's a wasted resource since you can manually add the `ip:port` of the script to Plex. * `discovery_address` may be helpful for making SSDP work properly. If `address` is not `0.0.0.0`, we will use that. If this is not set to a real IP, we won't run SSDP. SSDP is only really helpful for discovering in Plex/Emby. It's a wasted resource since you can manually add the `ip:port` of the script to Plex.
* `tuner_count` is a limit of devices able to stream from the script. * `tuner_count` is a limit of devices able to stream from the script. The default is 3, as per Locast's documentation. A 4th is possible, but is not reccomended.
* `friendlyname` is to set the name that Plex sees the script as. * `friendlyname` is to set the name that Plex sees the script as.
* `stream_type` can be set to `ffmpeg`, `vlc` or `direct`. * `reporting_*` are settings that show how the script projects itself as a hardware device.
* `device_auth` and `require_auth` are for an unimplemented Authentication feature.
* `chanscan_on_start` Scans Origin for new channels at startup.
```` ````
@ -43,61 +62,56 @@ The `fhdhr` contains all the configuration options for interfacing between this
# address = 0.0.0.0 # address = 0.0.0.0
# discovery_address = 0.0.0.0 # discovery_address = 0.0.0.0
# port = 5004 # port = 5004
# stream_type = direct
# tuner_count = 4 # tuner_count = 4
# friendlyname = fHDHR-NextPVR # friendlyname = fHDHR-Locast
# reporting_firmware_name = fHDHR_NextPVR # reporting_firmware_name = fHDHR_Locast
# reporting_manufacturer = BoronDust # reporting_manufacturer = BoronDust
# reporting_model = fHDHR # reporting_model = fHDHR
# reporting_firmware_ver = 20201001 # reporting_firmware_ver = 20201001
# reporting_tuner_type = Antenna # reporting_tuner_type = Antenna
# device_auth = fHDHR # device_auth = fHDHR
# require_auth = False
# chanscan_on_start = True
```` ````
# EPG # EPG
* `images` can be set to `proxy` or `pass`. If you choose `proxy`, images will be reverse proxied through fHDHR. * `images` can be set to `proxy` or `pass`. If you choose `proxy`, images will be reverse proxied through fHDHR.
* `method` defaults to `origin` and will pull the xmltv data from NextPVR. Other Options include `blocks` which is an hourly schedule with minimal channel information. Another option is `zap2it`, which is another source of EPG information. Channel Numbers may need to be manually mapped. * `method` defaults to `origin` and will pull the xmltv data from Locast. Other Options include `blocks` which is an hourly schedule with minimal channel information. Another option is `zap2it`, which is another source of EPG information. Channel Numbers may need to be manually mapped.
* `update_frequency` * `epg_update_frequency` determines how often we check for new scheduling information. In Seconds. * `update_frequency` determines how often we check for new scheduling information. In Seconds.
* `reverse_days` allows Blocks of EPG data to be created prior to the start of the EPG Source data.
* `forward_days` allows Blocks of EPG data to be created after the end of the EPG Source data.
* `block_size` in seconds, sets the default block size for data before, after and missing timeslots.
* `xmltv_offset` allows the final xmltv file to have an offset for users with timezone issues.
```` ````
[epg] [epg]
# images = pass # images = pass
# method = origin # method = origin
# update_frequency = 43200 # update_frequency = 43200
# reverse_days = -1
# forward_days = 7
# block_size = 1800
# xmltv_offset = +0000
```` ````
## ffmpeg ## ffmpeg
The `ffmpeg` section includes: The `ffmpeg` section includes:
* `path` is useful if ffmpeg is not in your systems PATH, or you want to manually specify. * `path` is useful if ffmpeg is not in your systems PATH, or you want to manually specify.
* `bytes_per_read` determines how many bytes of the stream to read before sending the data to your client. Increasing this value may cause longer load times, and lowering it may effect `stuttering`.
```` ````
[ffmpeg] [ffmpeg]
# path = ffmpeg # path = ffmpeg
# bytes_per_read = 1152000
```` ````
## vlc ## vlc
The `vlc` section includes: The `vlc` section includes:
* `path` is useful if ffmpeg is not in your systems PATH, or you want to manually specify. * `path` is useful if ffmpeg is not in your systems PATH, or you want to manually specify.
* `bytes_per_read` determines how many bytes of the stream to read before sending the data to your client. Increasing this value may cause longer load times, and lowering it may effect `stuttering`.
```` ````
[vlc] [vlc]
# path = ffmpeg # path = cvlc
# bytes_per_read = 1152000
````
## direct_stream
The `direct_stream` section is for when you set the `[fhdhr]stream_type` to `direct`
* `chunksize` is how much data to read at a time.
````
[direct_stream]
# chunksize = 1024*1024
```` ````
# Logging # Logging
@ -117,6 +131,27 @@ TODO: improve documentation here.
[database] [database]
# type = sqlite # type = sqlite
# driver = None # driver = None
user = None
pass = None
host = None
port = None
name = None
````
## RMG
````
# enabled = True
````
## SSDP
````
# enabled = True
# max_age = 1800
# proto = ipv6
# iface = None
# multicast_address = None
```` ````
## NextPVR ## NextPVR

View File

@ -1,37 +1,42 @@
# coding=utf-8 # coding=utf-8
from .originwrapper import OriginServiceWrapper
from .device import fHDHR_Device from .device import fHDHR_Device
from .api import fHDHR_API_URLs from .api import fHDHR_API_URLs
import fHDHR.tools import fHDHR.tools
fHDHR_VERSION = "v0.6.0-beta"
fHDHR_VERSION = "v0.4.6-beta"
class fHDHR_INT_OBJ(): class fHDHR_INT_OBJ():
def __init__(self, settings, logger, db): def __init__(self, settings, logger, db, plugins):
self.version = fHDHR_VERSION self.version = fHDHR_VERSION
self.config = settings self.config = settings
self.logger = logger self.logger = logger
self.db = db self.db = db
self.plugins = plugins
self.web = fHDHR.tools.WebReq() self.web = fHDHR.tools.WebReq()
for plugin_name in list(self.plugins.plugins.keys()):
self.plugins.plugins[plugin_name].plugin_utils.web = self.web
self.api = fHDHR_API_URLs(settings) self.api = fHDHR_API_URLs(settings, self.web)
for plugin_name in list(self.plugins.plugins.keys()):
self.plugins.plugins[plugin_name].plugin_utils.api = self.api
self.threads = {}
class fHDHR_OBJ(): class fHDHR_OBJ():
def __init__(self, settings, logger, db, origin, alternative_epg): def __init__(self, settings, logger, db, plugins):
self.fhdhr = fHDHR_INT_OBJ(settings, logger, db) self.fhdhr = fHDHR_INT_OBJ(settings, logger, db, plugins)
self.originwrapper = OriginServiceWrapper(self.fhdhr, origin) self.fhdhr.origins = fHDHR.origins.Origins(self.fhdhr)
self.device = fHDHR_Device(self.fhdhr, self.originwrapper, alternative_epg) self.device = fHDHR_Device(self.fhdhr, self.fhdhr.origins)
def __getattr__(self, name): def __getattr__(self, name):
''' will only get called for undefined attributes ''' ''' will only get called for undefined attributes '''
if hasattr(self.fhdhr, name): if hasattr(self.fhdhr, name):
return eval("self.fhdhr." + name) return eval("self.fhdhr.%s" % name)

View File

@ -1,15 +1,61 @@
import urllib.parse import urllib.parse
class Fillin_Client():
def __init__(self, settings, web):
self.config = settings
self.web = web
def __getattr__(self, name):
''' will only get called for undefined attributes '''
if hasattr(self.web.session, name):
return eval("self.web.session.%s" % name)
class fHDHR_API_URLs(): class fHDHR_API_URLs():
def __init__(self, settings): def __init__(self, settings, web):
self.config = settings self.config = settings
self.web = web
self.headers = {'User-Agent': "fHDHR/%s" % self.config.internal["versions"]["fHDHR"]}
# Replaced later
self.client = Fillin_Client(settings, web)
self.address = self.config.dict["fhdhr"]["address"] self.address = self.config.dict["fhdhr"]["address"]
self.discovery_address = self.config.dict["fhdhr"]["discovery_address"] self.discovery_address = self.config.dict["fhdhr"]["discovery_address"]
self.port = self.config.dict["fhdhr"]["port"] self.port = self.config.dict["fhdhr"]["port"]
def get(self, url, *args):
req_method = type(self.client).__name__
if not url.startswith("http"):
if not url.startswith("/"):
url = "/%s" % url
url = "%s%s" % (self.base, url)
if req_method == "FlaskClient":
self.client.get(url, headers=self.headers, *args)
else:
self.client.get(url, headers=self.headers, *args)
def post(self, url, *args):
req_method = type(self.client).__name__
if not url.startswith("http"):
if not url.startswith("/"):
url = "/%s" % url
url = "%s%s" % (self.base, url)
if req_method == "FlaskClient":
self.client.post(url, headers=self.headers, *args)
else:
self.client.post(url, headers=self.headers, *args)
@property @property
def base(self): def base(self):
if self.discovery_address: if self.discovery_address:

View File

@ -2,13 +2,13 @@ import os
import sys import sys
import argparse import argparse
import time import time
import multiprocessing
import threading
import platform
from fHDHR import fHDHR_VERSION, fHDHR_OBJ from fHDHR import fHDHR_VERSION, fHDHR_OBJ
import fHDHR.exceptions import fHDHR.exceptions
import fHDHR.config import fHDHR.config
import fHDHR.logger
import fHDHR.plugins
import fHDHR.origins
from fHDHR.db import fHDHRdb from fHDHR.db import fHDHRdb
ERR_CODE = 1 ERR_CODE = 1
@ -19,10 +19,6 @@ if sys.version_info.major == 2 or sys.version_info < (3, 7):
print('Error: fHDHR requires python 3.7+.') print('Error: fHDHR requires python 3.7+.')
sys.exit(1) sys.exit(1)
opersystem = platform.system()
if opersystem in ["Windows"]:
print("WARNING: This script may fail on Windows. Try Setting the `thread_method` to `threading`")
def build_args_parser(): def build_args_parser():
"""Build argument parser for fHDHR""" """Build argument parser for fHDHR"""
@ -31,48 +27,38 @@ def build_args_parser():
return parser.parse_args() return parser.parse_args()
def get_configuration(args, script_dir, origin, fHDHR_web): def get_configuration(args, script_dir, fHDHR_web):
if not os.path.isfile(args.cfg): if not os.path.isfile(args.cfg):
raise fHDHR.exceptions.ConfigurationNotFound(filename=args.cfg) raise fHDHR.exceptions.ConfigurationNotFound(filename=args.cfg)
return fHDHR.config.Config(args.cfg, script_dir, origin, fHDHR_web) return fHDHR.config.Config(args.cfg, script_dir, fHDHR_web)
def run(settings, logger, db, script_dir, fHDHR_web, origin, alternative_epg): def run(settings, logger, db, script_dir, fHDHR_web, plugins):
fhdhr = fHDHR_OBJ(settings, logger, db, origin, alternative_epg) fhdhr = fHDHR_OBJ(settings, logger, db, plugins)
fhdhrweb = fHDHR_web.fHDHR_HTTP_Server(fhdhr) fhdhrweb = fHDHR_web.fHDHR_HTTP_Server(fhdhr)
try: try:
print("HTTP Server Starting") # Start Flask Thread
if settings.dict["main"]["thread_method"] in ["multiprocessing"]: fhdhrweb.start()
fhdhr_web = multiprocessing.Process(target=fhdhrweb.run)
elif settings.dict["main"]["thread_method"] in ["threading"]:
fhdhr_web = threading.Thread(target=fhdhrweb.run)
if settings.dict["main"]["thread_method"] in ["multiprocessing", "threading"]:
fhdhr_web.start()
# Start SSDP Thread
if settings.dict["fhdhr"]["discovery_address"]: if settings.dict["fhdhr"]["discovery_address"]:
print("SSDP Server Starting") fhdhr.device.ssdp.start()
if settings.dict["main"]["thread_method"] in ["multiprocessing"]:
fhdhr_ssdp = multiprocessing.Process(target=fhdhr.device.ssdp.run)
elif settings.dict["main"]["thread_method"] in ["threading"]:
fhdhr_ssdp = threading.Thread(target=fhdhr.device.ssdp.run)
if settings.dict["main"]["thread_method"] in ["multiprocessing", "threading"]:
fhdhr_ssdp.start()
# Start EPG Thread
if settings.dict["epg"]["method"]: if settings.dict["epg"]["method"]:
print("EPG Update Starting") fhdhr.device.epg.start()
if settings.dict["main"]["thread_method"] in ["multiprocessing"]:
fhdhr_epg = multiprocessing.Process(target=fhdhr.device.epg.run) # Perform some actions now that HTTP Server is running
elif settings.dict["main"]["thread_method"] in ["threading"]: fhdhr.api.get("/api/startup_tasks")
fhdhr_epg = threading.Thread(target=fhdhr.device.epg.run)
if settings.dict["main"]["thread_method"] in ["multiprocessing", "threading"]:
fhdhr_epg.start()
# wait forever # wait forever
while True: restart_code = "restart"
time.sleep(3600) while fhdhr.threads["flask"].is_alive():
time.sleep(1)
return restart_code
except KeyboardInterrupt: except KeyboardInterrupt:
return ERR_CODE_NO_RESTART return ERR_CODE_NO_RESTART
@ -80,32 +66,52 @@ def run(settings, logger, db, script_dir, fHDHR_web, origin, alternative_epg):
return ERR_CODE return ERR_CODE
def start(args, script_dir, fHDHR_web, origin, alternative_epg): def start(args, script_dir, fHDHR_web):
"""Get Configuration for fHDHR and start""" """Get Configuration for fHDHR and start"""
try: try:
settings = get_configuration(args, script_dir, origin, fHDHR_web) settings = get_configuration(args, script_dir, fHDHR_web)
except fHDHR.exceptions.ConfigurationError as e: except fHDHR.exceptions.ConfigurationError as e:
print(e) print(e)
return ERR_CODE_NO_RESTART return ERR_CODE_NO_RESTART
logger = settings.logging_setup() # Find Plugins and import their default configs
plugins = fHDHR.plugins.PluginsHandler(settings)
# Apply User Configuration
settings.user_config()
settings.config_verification()
# Setup Logging
logger = fHDHR.logger.Logger(settings)
# Setup Database
db = fHDHRdb(settings) db = fHDHRdb(settings)
return run(settings, logger, db, script_dir, fHDHR_web, origin, alternative_epg) # Setup Plugins
plugins.load_plugins(logger, db)
plugins.setup()
settings.config_verification_plugins()
if not len([x for x in list(plugins.plugins.keys()) if plugins.plugins[x].type == "origin"]):
print("No Origin Plugins found.")
return ERR_CODE
return run(settings, logger, db, script_dir, fHDHR_web, plugins)
def main(script_dir, fHDHR_web, origin, alternative_epg): def main(script_dir, fHDHR_web):
"""fHDHR run script entry point""" """fHDHR run script entry point"""
print("Loading fHDHR %s" % fHDHR_VERSION) print("Loading fHDHR %s" % fHDHR_VERSION)
print("Loading fHDHR_web %s" % fHDHR_web.fHDHR_web_VERSION) print("Loading fHDHR_web %s" % fHDHR_web.fHDHR_web_VERSION)
print("Loading Origin Service: %s %s" % (origin.ORIGIN_NAME, origin.ORIGIN_VERSION))
try: try:
args = build_args_parser() args = build_args_parser()
return start(args, script_dir, fHDHR_web, origin, alternative_epg) while True:
returned_code = start(args, script_dir, fHDHR_web)
if returned_code not in ["restart"]:
return returned_code
except KeyboardInterrupt: except KeyboardInterrupt:
print("\n\nInterrupted") print("\n\nInterrupted")
return ERR_CODE return ERR_CODE

View File

@ -3,8 +3,6 @@ import sys
import random import random
import configparser import configparser
import pathlib import pathlib
import logging
import subprocess
import platform import platform
import json import json
@ -15,31 +13,31 @@ from fHDHR.tools import isint, isfloat, is_arithmetic, is_docker
class Config(): class Config():
def __init__(self, filename, script_dir, origin, fHDHR_web): def __init__(self, filename, script_dir, fHDHR_web):
self.origin = origin
self.fHDHR_web = fHDHR_web self.fHDHR_web = fHDHR_web
self.internal = {} self.internal = {}
self.conf_default = {} self.conf_default = {}
self.dict = {} self.dict = {}
self.internal["versions"] = {}
self.config_file = filename self.config_file = filename
self.initial_load(script_dir) self.core_setup(script_dir)
self.config_verification()
def initial_load(self, script_dir): def core_setup(self, script_dir):
data_dir = pathlib.Path(script_dir).joinpath('data') data_dir = pathlib.Path(script_dir).joinpath('data')
internal_plugins_dir = pathlib.Path(script_dir).joinpath('plugins')
fHDHR_web_dir = pathlib.Path(script_dir).joinpath('fHDHR_web') fHDHR_web_dir = pathlib.Path(script_dir).joinpath('fHDHR_web')
www_dir = pathlib.Path(fHDHR_web_dir).joinpath('www_dir') www_dir = pathlib.Path(fHDHR_web_dir).joinpath('www_dir')
self.internal["paths"] = { self.internal["paths"] = {
"script_dir": script_dir, "script_dir": script_dir,
"data_dir": data_dir, "data_dir": data_dir,
"alternative_epg": pathlib.Path(script_dir).joinpath('alternative_epg'), "plugins_dir": [internal_plugins_dir],
"origin": pathlib.Path(script_dir).joinpath('origin'),
"cache_dir": pathlib.Path(data_dir).joinpath('cache'), "cache_dir": pathlib.Path(data_dir).joinpath('cache'),
"internal_config": pathlib.Path(data_dir).joinpath('internal_config'), "internal_config": pathlib.Path(data_dir).joinpath('internal_config'),
"fHDHR_web_dir": fHDHR_web_dir,
"www_dir": www_dir, "www_dir": www_dir,
"www_templates_dir": pathlib.Path(fHDHR_web_dir).joinpath('templates'), "www_templates_dir": pathlib.Path(fHDHR_web_dir).joinpath('templates'),
"font": pathlib.Path(data_dir).joinpath('garamond.ttf'), "font": pathlib.Path(data_dir).joinpath('garamond.ttf'),
@ -50,32 +48,66 @@ class Config():
if str(conffilepath).endswith(".json"): if str(conffilepath).endswith(".json"):
self.read_json_config(conffilepath) self.read_json_config(conffilepath)
for dir_type in ["alternative_epg", "origin"]: for file_item in os.listdir(self.internal["paths"]["fHDHR_web_dir"]):
file_item_path = pathlib.Path(self.internal["paths"]["fHDHR_web_dir"]).joinpath(file_item)
for file_item in os.listdir(self.internal["paths"][dir_type]):
file_item_path = os.path.join(self.internal["paths"][dir_type], file_item)
if str(file_item_path).endswith("_conf.json"): if str(file_item_path).endswith("_conf.json"):
self.read_json_config(file_item_path) self.read_json_config(file_item_path)
print("Loading Configuration File: " + str(self.config_file)) self.dict["epg"]["valid_methods"] = {None: {}}
self.read_ini_config(self.config_file) self.dict["origins"] = {}
self.dict["origins"]["valid_methods"] = {}
self.dict["streaming"]["valid_methods"] = {"direct": {}}
self.dict["plugin_web_paths"] = {}
self.load_versions() self.load_versions()
def register_web_path(self, name, path, plugin_dict_name):
self.dict["plugin_web_paths"][name.lower()] = {
"name": name,
"namespace": name.lower(),
"path": path,
"plugin": plugin_dict_name
}
def register_valid_origin_method(self, method_item):
self.dict["origins"]["valid_methods"][method_item.lower()] = {
"name": method_item,
"namespace": method_item.lower(),
}
def register_valid_streaming_method(self, method_item, plugin_dict_name):
self.dict["streaming"]["valid_methods"][method_item.lower()] = {
"name": method_item,
"namespace": method_item.lower(),
"plugin": plugin_dict_name
}
def register_valid_epg_method(self, method_item, plugin_dict_name):
self.dict["epg"]["valid_methods"][method_item.lower()] = {
"name": method_item,
"namespace": method_item.lower(),
"plugin": plugin_dict_name
}
def register_version(self, item_name, item_version, item_type):
self.internal["versions"][item_name] = {
"name": item_name,
"version": item_version,
"type": item_type
}
def import_conf_json(self, file_item_path):
self.read_json_config(file_item_path)
def load_versions(self): def load_versions(self):
self.internal["versions"] = {} self.register_version("fHDHR", fHDHR_VERSION, "fHDHR")
self.register_version("fHDHR_web", self.fHDHR_web.fHDHR_web_VERSION, "fHDHR")
self.internal["versions"]["fHDHR"] = fHDHR_VERSION self.register_version("Python", sys.version, "env")
self.internal["versions"]["fHDHR_web"] = self.fHDHR_web.fHDHR_web_VERSION
self.internal["versions"][self.origin.ORIGIN_NAME] = self.origin.ORIGIN_VERSION
self.internal["versions"]["Python"] = sys.version
opersystem = platform.system() opersystem = platform.system()
self.internal["versions"]["Operating System"] = opersystem self.register_version("Operating System", opersystem, "env")
if opersystem in ["Linux", "Darwin"]: if opersystem in ["Linux", "Darwin"]:
# Linux/Mac # Linux/Mac
if os.getuid() == 0 or os.geteuid() == 0: if os.getuid() == 0 or os.geteuid() == 0:
@ -88,41 +120,90 @@ class Config():
print("Uncommon Operating System, use at your own risk.") print("Uncommon Operating System, use at your own risk.")
isdocker = is_docker() isdocker = is_docker()
self.internal["versions"]["Docker"] = isdocker self.register_version("Docker", isdocker, "env")
if self.dict["fhdhr"]["stream_type"] == "ffmpeg": def user_config(self):
try: print("Loading Configuration File: %s" % self.config_file)
ffmpeg_command = [self.dict["ffmpeg"]["path"], self.read_ini_config(self.config_file)
"-version",
"pipe:stdout"
]
ffmpeg_proc = subprocess.Popen(ffmpeg_command, stdout=subprocess.PIPE) def config_verification_plugins(self):
ffmpeg_version = ffmpeg_proc.stdout.read() required_missing = {}
ffmpeg_proc.terminate() # create dict and combine items
ffmpeg_proc.communicate() for config_section in list(self.conf_default.keys()):
ffmpeg_version = ffmpeg_version.decode().split("version ")[1].split(" ")[0] for config_item in list(self.conf_default[config_section].keys()):
except FileNotFoundError: if self.conf_default[config_section][config_item]["required"]:
ffmpeg_version = "Missing" if not self.dict[config_section][config_item]:
print("Failed to find ffmpeg.") if config_section not in list(required_missing.keys()):
self.internal["versions"]["ffmpeg"] = ffmpeg_version required_missing[config_section] = []
required_missing[config_section].append(config_item)
for config_section in list(required_missing.keys()):
print("Warning! Required configuration options missing: [%s]%s" % (config_section, ", ".join(required_missing[config_section])))
if self.dict["fhdhr"]["stream_type"] == "vlc": if self.dict["epg"]["method"] and self.dict["epg"]["method"] not in ["None"]:
try: if isinstance(self.dict["epg"]["method"], str):
vlc_command = [self.dict["vlc"]["path"], self.dict["epg"]["method"] = [self.dict["epg"]["method"]]
"--version", epg_methods = []
"pipe:stdout" for epg_method in self.dict["epg"]["method"]:
] if epg_method in list(self.dict["epg"]["valid_methods"].keys()):
epg_methods.append(epg_method)
elif epg_method in list(self.dict["origins"]["valid_methods"].keys()):
epg_methods.append(epg_method)
else:
raise fHDHR.exceptions.ConfigurationError("Invalid EPG Method. Exiting...")
if self.dict["epg"]["method"]:
self.dict["epg"]["def_method"] = self.dict["epg"]["method"][0]
else:
self.dict["epg"]["def_method"] = None
vlc_proc = subprocess.Popen(vlc_command, stdout=subprocess.PIPE) if self.dict["streaming"]["method"] not in self.dict["streaming"]["valid_methods"]:
vlc_version = vlc_proc.stdout.read() raise fHDHR.exceptions.ConfigurationError("Invalid stream type. Exiting...")
vlc_proc.terminate()
vlc_proc.communicate() def config_verification(self):
vlc_version = vlc_version.decode().split("version ")[1].split('\n')[0]
except FileNotFoundError: if not self.dict["main"]["uuid"]:
vlc_version = "Missing" self.dict["main"]["uuid"] = ''.join(random.choice("hijklmnopqrstuvwxyz") for i in range(8))
print("Failed to find vlc.") self.write('uuid', self.dict["main"]["uuid"], 'main')
self.internal["versions"]["vlc"] = vlc_version
if self.dict["main"]["cache_dir"]:
if not pathlib.Path(self.dict["main"]["cache_dir"]).is_dir():
raise fHDHR.exceptions.ConfigurationError("Invalid Cache Directory. Exiting...")
self.internal["paths"]["cache_dir"] = pathlib.Path(self.dict["main"]["cache_dir"])
cache_dir = self.internal["paths"]["cache_dir"]
logs_dir = pathlib.Path(cache_dir).joinpath('logs')
self.internal["paths"]["logs_dir"] = logs_dir
if not logs_dir.is_dir():
logs_dir.mkdir()
self.dict["database"]["path"] = pathlib.Path(cache_dir).joinpath('fhdhr.db')
if not self.dict["fhdhr"]["discovery_address"] and self.dict["fhdhr"]["address"] != "0.0.0.0":
self.dict["fhdhr"]["discovery_address"] = self.dict["fhdhr"]["address"]
if not self.dict["fhdhr"]["discovery_address"] or self.dict["fhdhr"]["discovery_address"] == "0.0.0.0":
self.dict["fhdhr"]["discovery_address"] = None
def get_real_conf_value(self, key, confvalue):
if not confvalue:
confvalue = None
elif key == "xmltv_offset":
confvalue = str(confvalue)
elif str(confvalue) in ["0"]:
confvalue = 0
elif isint(confvalue):
confvalue = int(confvalue)
elif isfloat(confvalue):
confvalue = float(confvalue)
elif is_arithmetic(confvalue):
confvalue = eval(confvalue)
elif "," in confvalue:
confvalue = confvalue.split(",")
elif str(confvalue).lower() in ["none", ""]:
confvalue = None
elif str(confvalue).lower() in ["false"]:
confvalue = False
elif str(confvalue).lower() in ["true"]:
confvalue = True
return confvalue
def read_json_config(self, conffilepath): def read_json_config(self, conffilepath):
with open(conffilepath, 'r') as jsonconf: with open(conffilepath, 'r') as jsonconf:
@ -140,27 +221,13 @@ class Config():
if key not in list(self.conf_default[section].keys()): if key not in list(self.conf_default[section].keys()):
self.conf_default[section][key] = {} self.conf_default[section][key] = {}
confvalue = confimport[section][key]["value"] confvalue = self.get_real_conf_value(key, confimport[section][key]["value"])
if isint(confvalue):
confvalue = int(confvalue)
elif isfloat(confvalue):
confvalue = float(confvalue)
elif is_arithmetic(confvalue):
confvalue = eval(confvalue)
elif "," in confvalue:
confvalue = confvalue.split(",")
elif str(confvalue).lower() in ["none"]:
confvalue = None
elif str(confvalue).lower() in ["false"]:
confvalue = False
elif str(confvalue).lower() in ["true"]:
confvalue = True
self.dict[section][key] = confvalue self.dict[section][key] = confvalue
self.conf_default[section][key]["value"] = confvalue self.conf_default[section][key]["value"] = confvalue
for config_option in ["config_web_hidden", "config_file", "config_web"]: for config_option in ["config_web_hidden", "config_file", "config_web", "required"]:
if config_option not in list(confimport[section][key].keys()): if config_option not in list(confimport[section][key].keys()):
config_option_value = False config_option_value = False
else: else:
@ -180,22 +247,7 @@ class Config():
if each_section.lower() not in list(self.dict.keys()): if each_section.lower() not in list(self.dict.keys()):
self.dict[each_section.lower()] = {} self.dict[each_section.lower()] = {}
for (each_key, each_val) in config_handler.items(each_section): for (each_key, each_val) in config_handler.items(each_section):
if not each_val: each_val = self.get_real_conf_value(each_key, each_val)
each_val = None
elif each_val.lower() in ["none"]:
each_val = None
elif each_val.lower() in ["false"]:
each_val = False
elif each_val.lower() in ["true"]:
each_val = True
elif isint(each_val):
each_val = int(each_val)
elif isfloat(each_val):
each_val = float(each_val)
elif is_arithmetic(each_val):
each_val = eval(each_val)
elif "," in each_val:
each_val = each_val.split(",")
import_val = True import_val = True
if each_section in list(self.conf_default.keys()): if each_section in list(self.conf_default.keys()):
@ -206,10 +258,23 @@ class Config():
if import_val: if import_val:
self.dict[each_section.lower()][each_key.lower()] = each_val self.dict[each_section.lower()][each_key.lower()] = each_val
def write(self, section, key, value): def write(self, key, value, section):
if section == self.dict["main"]["dictpopname"]:
self.dict["origin"][key] = value if not value:
else: value = None
if value.lower() in ["none"]:
value = None
elif value.lower() in ["false"]:
value = False
elif value.lower() in ["true"]:
value = True
elif isint(value):
value = int(value)
elif isfloat(value):
value = float(value)
elif isinstance(value, list):
",".join(value)
self.dict[section][key] = value self.dict[section][key] = value
config_handler = configparser.ConfigParser() config_handler = configparser.ConfigParser()
@ -218,100 +283,11 @@ class Config():
if not config_handler.has_section(section): if not config_handler.has_section(section):
config_handler.add_section(section) config_handler.add_section(section)
config_handler.set(section, key, value) config_handler.set(section, key, str(value))
with open(self.config_file, 'w') as config_file: with open(self.config_file, 'w') as config_file:
config_handler.write(config_file) config_handler.write(config_file)
def config_verification(self):
if self.dict["main"]["thread_method"] not in ["threading", "multiprocessing"]:
raise fHDHR.exceptions.ConfigurationError("Invalid Threading Method. Exiting...")
if self.dict["main"]["required"]:
required_missing = []
if isinstance(self.dict["main"]["required"], str):
self.dict["main"]["required"] = [self.dict["main"]["required"]]
if len(self.dict["main"]["required"]):
for req_item in self.dict["main"]["required"]:
req_section = req_item.split("/")[0]
req_key = req_item.split("/")[1]
if not self.dict[req_section][req_key]:
required_missing.append(req_item)
if len(required_missing):
raise fHDHR.exceptions.ConfigurationError("Required configuration options missing: " + ", ".join(required_missing))
self.dict["origin"] = self.dict.pop(self.dict["main"]["dictpopname"])
if isinstance(self.dict["main"]["valid_epg_methods"], str):
self.dict["main"]["valid_epg_methods"] = [self.dict["main"]["valid_epg_methods"]]
if self.dict["epg"]["method"] and self.dict["epg"]["method"] not in ["None"]:
if isinstance(self.dict["epg"]["method"], str):
self.dict["epg"]["method"] = [self.dict["epg"]["method"]]
epg_methods = []
for epg_method in self.dict["epg"]["method"]:
if epg_method == self.dict["main"]["dictpopname"] or epg_method == "origin":
epg_methods.append("origin")
elif epg_method in ["None"]:
raise fHDHR.exceptions.ConfigurationError("Invalid EPG Method. Exiting...")
elif epg_method in self.dict["main"]["valid_epg_methods"]:
epg_methods.append(epg_method)
else:
raise fHDHR.exceptions.ConfigurationError("Invalid EPG Method. Exiting...")
self.dict["epg"]["def_method"] = self.dict["epg"]["method"][0]
if not self.dict["main"]["uuid"]:
self.dict["main"]["uuid"] = ''.join(random.choice("hijklmnopqrstuvwxyz") for i in range(8))
self.write('main', 'uuid', self.dict["main"]["uuid"])
if self.dict["main"]["cache_dir"]:
if not pathlib.Path(self.dict["main"]["cache_dir"]).is_dir():
raise fHDHR.exceptions.ConfigurationError("Invalid Cache Directory. Exiting...")
self.internal["paths"]["cache_dir"] = pathlib.Path(self.dict["main"]["cache_dir"])
cache_dir = self.internal["paths"]["cache_dir"]
logs_dir = pathlib.Path(cache_dir).joinpath('logs')
self.internal["paths"]["logs_dir"] = logs_dir
if not logs_dir.is_dir():
logs_dir.mkdir()
self.dict["database"]["path"] = pathlib.Path(cache_dir).joinpath('fhdhr.db')
if self.dict["fhdhr"]["stream_type"] not in ["direct", "ffmpeg", "vlc"]:
raise fHDHR.exceptions.ConfigurationError("Invalid stream type. Exiting...")
if not self.dict["fhdhr"]["discovery_address"] and self.dict["fhdhr"]["address"] != "0.0.0.0":
self.dict["fhdhr"]["discovery_address"] = self.dict["fhdhr"]["address"]
if not self.dict["fhdhr"]["discovery_address"] or self.dict["fhdhr"]["discovery_address"] == "0.0.0.0":
self.dict["fhdhr"]["discovery_address"] = None
def logging_setup(self):
log_level = self.dict["logging"]["level"].upper()
# Create a custom logger
logging.basicConfig(format='%(name)s - %(levelname)s - %(message)s', level=log_level)
logger = logging.getLogger('fHDHR')
log_file = os.path.join(self.internal["paths"]["logs_dir"], 'fHDHR.log')
# Create handlers
# c_handler = logging.StreamHandler()
f_handler = logging.FileHandler(log_file)
# c_handler.setLevel(log_level)
f_handler.setLevel(log_level)
# Create formatters and add it to handlers
# c_format = logging.Formatter('%(name)s - %(levelname)s - %(message)s')
f_format = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
# c_handler.setFormatter(c_format)
f_handler.setFormatter(f_format)
# Add handlers to the logger
# logger.addHandler(c_handler)
logger.addHandler(f_handler)
return logger
def __getattr__(self, name): def __getattr__(self, name):
''' will only get called for undefined attributes ''' ''' will only get called for undefined attributes '''
if name in list(self.dict.keys()): if name in list(self.dict.keys()):

View File

@ -32,28 +32,10 @@ MYSQL_TABLE_ARGS = {'mysql_engine': 'InnoDB',
'mysql_collate': 'utf8mb4_unicode_ci'} 'mysql_collate': 'utf8mb4_unicode_ci'}
class ChannelValues(BASE): class PluginValues(BASE):
__tablename__ = 'channel_values' __tablename__ = 'plugin_values'
__table_args__ = MYSQL_TABLE_ARGS __table_args__ = MYSQL_TABLE_ARGS
channel = Column(String(255), primary_key=True) pluginitem = Column(String(255), primary_key=True)
namespace = Column(String(255), primary_key=True)
key = Column(String(255), primary_key=True)
value = Column(Text())
class ProgramValues(BASE):
__tablename__ = 'program_values'
__table_args__ = MYSQL_TABLE_ARGS
program = Column(String(255), primary_key=True)
namespace = Column(String(255), primary_key=True)
key = Column(String(255), primary_key=True)
value = Column(Text())
class CacheValues(BASE):
__tablename__ = 'cache_values'
__table_args__ = MYSQL_TABLE_ARGS
cacheitem = Column(String(255), primary_key=True)
namespace = Column(String(255), primary_key=True) namespace = Column(String(255), primary_key=True)
key = Column(String(255), primary_key=True) key = Column(String(255), primary_key=True)
value = Column(Text()) value = Column(Text())
@ -148,198 +130,6 @@ class fHDHRdb(object):
def get_uri(self): def get_uri(self):
return self.url return self.url
# Channel Values
def set_channel_value(self, channel, key, value, namespace='default'):
channel = channel.lower()
value = json.dumps(value, ensure_ascii=False)
session = self.ssession()
try:
result = session.query(ChannelValues) \
.filter(ChannelValues.channel == channel)\
.filter(ChannelValues.namespace == namespace)\
.filter(ChannelValues.key == key) \
.one_or_none()
# ChannelValues exists, update
if result:
result.value = value
session.commit()
# DNE - Insert
else:
new_channelvalue = ChannelValues(channel=channel, namespace=namespace, key=key, value=value)
session.add(new_channelvalue)
session.commit()
except SQLAlchemyError:
session.rollback()
raise
finally:
session.close()
def get_channel_value(self, channel, key, namespace='default'):
channel = channel.lower()
session = self.ssession()
try:
result = session.query(ChannelValues) \
.filter(ChannelValues.channel == channel)\
.filter(ChannelValues.namespace == namespace)\
.filter(ChannelValues.key == key) \
.one_or_none()
if result is not None:
result = result.value
return _deserialize(result)
except SQLAlchemyError:
session.rollback()
raise
finally:
session.close()
def delete_channel_value(self, channel, key, namespace='default'):
channel = channel.lower()
session = self.ssession()
try:
result = session.query(ChannelValues) \
.filter(ChannelValues.channel == channel)\
.filter(ChannelValues.namespace == namespace)\
.filter(ChannelValues.key == key) \
.one_or_none()
# ChannelValues exists, delete
if result:
session.delete(result)
session.commit()
except SQLAlchemyError:
session.rollback()
raise
finally:
session.close()
# Program Values
def set_program_value(self, program, key, value, namespace='default'):
program = program.lower()
value = json.dumps(value, ensure_ascii=False)
session = self.ssession()
try:
result = session.query(ProgramValues) \
.filter(ProgramValues.program == program)\
.filter(ProgramValues.namespace == namespace)\
.filter(ProgramValues.key == key) \
.one_or_none()
# ProgramValue exists, update
if result:
result.value = value
session.commit()
# DNE - Insert
else:
new_programvalue = ProgramValues(program=program, namespace=namespace, key=key, value=value)
session.add(new_programvalue)
session.commit()
except SQLAlchemyError:
session.rollback()
raise
finally:
session.close()
def get_program_value(self, program, key, namespace='default'):
program = program.lower()
session = self.ssession()
try:
result = session.query(ProgramValues) \
.filter(ProgramValues.program == program)\
.filter(ProgramValues.namespace == namespace)\
.filter(ProgramValues.key == key) \
.one_or_none()
if result is not None:
result = result.value
return _deserialize(result)
except SQLAlchemyError:
session.rollback()
raise
finally:
session.close()
def delete_program_value(self, program, key, namespace='default'):
program = program.lower()
session = self.ssession()
try:
result = session.query(ProgramValues) \
.filter(ProgramValues.program == program)\
.filter(ProgramValues.namespace == namespace)\
.filter(ProgramValues.key == key) \
.one_or_none()
# ProgramValue exists, delete
if result:
session.delete(result)
session.commit()
except SQLAlchemyError:
session.rollback()
raise
finally:
session.close()
# Cache Values
def set_cacheitem_value(self, cacheitem, key, value, namespace='default'):
cacheitem = cacheitem.lower()
value = json.dumps(value, ensure_ascii=False)
session = self.ssession()
try:
result = session.query(CacheValues) \
.filter(CacheValues.cacheitem == cacheitem)\
.filter(CacheValues.namespace == namespace)\
.filter(CacheValues.key == key) \
.one_or_none()
# ProgramValue exists, update
if result:
result.value = value
session.commit()
# DNE - Insert
else:
new_cacheitemvalue = CacheValues(cacheitem=cacheitem, namespace=namespace, key=key, value=value)
session.add(new_cacheitemvalue)
session.commit()
except SQLAlchemyError:
session.rollback()
raise
finally:
session.close()
def get_cacheitem_value(self, cacheitem, key, namespace='default'):
cacheitem = cacheitem.lower()
session = self.ssession()
try:
result = session.query(CacheValues) \
.filter(CacheValues.cacheitem == cacheitem)\
.filter(CacheValues.namespace == namespace)\
.filter(CacheValues.key == key) \
.one_or_none()
if result is not None:
result = result.value
return _deserialize(result)
except SQLAlchemyError:
session.rollback()
raise
finally:
session.close()
def delete_cacheitem_value(self, cacheitem, key, namespace='default'):
cacheitem = cacheitem.lower()
session = self.ssession()
try:
result = session.query(CacheValues) \
.filter(CacheValues.cacheitem == cacheitem)\
.filter(CacheValues.namespace == namespace)\
.filter(CacheValues.key == key) \
.one_or_none()
# ProgramValue exists, delete
if result:
session.delete(result)
session.commit()
except SQLAlchemyError:
session.rollback()
raise
finally:
session.close()
# fHDHR Values # fHDHR Values
def set_fhdhr_value(self, item, key, value, namespace='default'): def set_fhdhr_value(self, item, key, value, namespace='default'):
@ -358,8 +148,8 @@ class fHDHRdb(object):
session.commit() session.commit()
# DNE - Insert # DNE - Insert
else: else:
new_cacheitemvalue = fHDHRValues(item=item, namespace=namespace, key=key, value=value) new_pluginitemvalue = fHDHRValues(item=item, namespace=namespace, key=key, value=value)
session.add(new_cacheitemvalue) session.add(new_pluginitemvalue)
session.commit() session.commit()
except SQLAlchemyError: except SQLAlchemyError:
session.rollback() session.rollback()
@ -403,3 +193,67 @@ class fHDHRdb(object):
raise raise
finally: finally:
session.close() session.close()
# Plugin Values
def set_plugin_value(self, pluginitem, key, value, namespace='default'):
pluginitem = pluginitem.lower()
value = json.dumps(value, ensure_ascii=False)
session = self.ssession()
try:
result = session.query(PluginValues) \
.filter(PluginValues.pluginitem == pluginitem)\
.filter(PluginValues.namespace == namespace)\
.filter(PluginValues.key == key) \
.one_or_none()
# ProgramValue exists, update
if result:
result.value = value
session.commit()
# DNE - Insert
else:
new_pluginitemvalue = PluginValues(pluginitem=pluginitem, namespace=namespace, key=key, value=value)
session.add(new_pluginitemvalue)
session.commit()
except SQLAlchemyError:
session.rollback()
raise
finally:
session.close()
def get_plugin_value(self, pluginitem, key, namespace='default'):
pluginitem = pluginitem.lower()
session = self.ssession()
try:
result = session.query(PluginValues) \
.filter(PluginValues.pluginitem == pluginitem)\
.filter(PluginValues.namespace == namespace)\
.filter(PluginValues.key == key) \
.one_or_none()
if result is not None:
result = result.value
return _deserialize(result)
except SQLAlchemyError:
session.rollback()
raise
finally:
session.close()
def delete_plugin_value(self, pluginitem, key, namespace='default'):
pluginitem = pluginitem.lower()
session = self.ssession()
try:
result = session.query(PluginValues) \
.filter(PluginValues.pluginitem == pluginitem)\
.filter(PluginValues.namespace == namespace)\
.filter(PluginValues.key == key) \
.one_or_none()
# ProgramValue exists, delete
if result:
session.delete(result)
session.commit()
except SQLAlchemyError:
session.rollback()
raise
finally:
session.close()

View File

@ -3,16 +3,16 @@ from .epg import EPG
from .tuners import Tuners from .tuners import Tuners
from .images import imageHandler from .images import imageHandler
from .ssdp import SSDPServer from .ssdp import SSDPServer
from .cluster import fHDHR_Cluster
class fHDHR_Device(): class fHDHR_Device():
def __init__(self, fhdhr, originwrapper, alternative_epg): def __init__(self, fhdhr, origins):
self.fhdhr = fhdhr
self.channels = Channels(fhdhr, originwrapper) self.channels = Channels(fhdhr, origins)
self.epg = EPG(fhdhr, self.channels, originwrapper, alternative_epg) self.epg = EPG(fhdhr, self.channels, origins)
self.tuners = Tuners(fhdhr, self.epg, self.channels) self.tuners = Tuners(fhdhr, self.epg, self.channels)
@ -20,4 +20,15 @@ class fHDHR_Device():
self.ssdp = SSDPServer(fhdhr) self.ssdp = SSDPServer(fhdhr)
self.cluster = fHDHR_Cluster(fhdhr, self.ssdp) self.interfaces = {}
for plugin_name in list(self.fhdhr.plugins.plugins.keys()):
if self.fhdhr.plugins.plugins[plugin_name].manifest["type"] == "interface":
method = self.fhdhr.plugins.plugins[plugin_name].name.lower()
plugin_utils = self.fhdhr.plugins.plugins[plugin_name].plugin_utils
plugin_utils.channels = self.channels
plugin_utils.epg = self.epg
plugin_utils.tuners = self.tuners
plugin_utils.images = self.images
plugin_utils.ssdp = self.ssdp
self.interfaces[method] = self.fhdhr.plugins.plugins[plugin_name].Plugin_OBJ(fhdhr, plugin_utils)

View File

@ -1,7 +1,6 @@
import datetime
import time import time
from fHDHR.tools import hours_between_datetime from fHDHR.tools import humanized_time
from .channel import Channel from .channel import Channel
from .chan_ident import Channel_IDs from .chan_ident import Channel_IDs
@ -9,51 +8,111 @@ from .chan_ident import Channel_IDs
class Channels(): class Channels():
def __init__(self, fhdhr, originwrapper): def __init__(self, fhdhr, origins):
self.fhdhr = fhdhr self.fhdhr = fhdhr
self.origin = originwrapper self.origins = origins
self.id_system = Channel_IDs(fhdhr) self.id_system = Channel_IDs(fhdhr, origins)
self.list = {} self.list = {}
self.list_update_time = None for origin in list(self.origins.origins_dict.keys()):
self.list[origin] = {}
self.get_db_channels() self.get_db_channels()
haseverscanned = self.fhdhr.db.get_fhdhr_value("channels", "scanned_time")
if (self.fhdhr.config.dict["fhdhr"]["chanscan_on_start"] or not haseverscanned):
self.get_channels()
def get_channel_obj(self, keyfind, valfind): def get_channel_obj(self, keyfind, valfind, origin=None):
return next(self.list[fhdhr_id] for fhdhr_id in list(self.list.keys()) if self.list[fhdhr_id].dict[keyfind] == valfind) if origin:
origin = origin.lower()
if keyfind == "number":
matches = [self.list[origin][x].dict["id"] for x in list(self.list[origin].keys()) if self.list[origin][x].number == valfind]
else:
matches = [self.list[origin][x].dict["id"] for x in list(self.list[origin].keys()) if self.list[origin][x].dict[keyfind] == valfind]
if len(matches):
return self.list[origin][matches[0]]
else:
matches = []
for origin in list(self.list.keys()):
if keyfind == "number":
matches = [self.list[origin][x].dict["id"] for x in list(self.list[origin].keys()) if self.list[origin][x].number == valfind]
else:
matches = [self.list[origin][x].dict["id"] for x in list(self.list[origin].keys()) if self.list[origin][x].dict[keyfind] == valfind]
if len(matches):
return self.list[origin][matches[0]]
if len(matches):
return self.list[origin][matches[0]]
return None
def get_channel_list(self, keyfind): def get_channel_list(self, keyfind, origin=None):
return [self.list[x].dict[keyfind] for x in list(self.list.keys())] if origin:
if keyfind == "number":
return [self.list[origin][x].number for x in [x["id"] for x in self.get_channels(origin)]]
else:
return [self.list[origin][x].dict[keyfind] for x in [x["id"] for x in self.get_channels(origin)]]
else:
matches = []
for origin in list(self.list.keys()):
if keyfind == "number":
next_match = [self.list[origin][x].number for x in [x["id"] for x in self.get_channels(origin)]]
else:
next_match = [self.list[origin][x].dict[keyfind] for x in [x["id"] for x in self.get_channels(origin)]]
if len(next_match):
matches.append(next_match)
return matches[0]
def set_channel_status(self, keyfind, valfind, updatedict): def get_channel_dict(self, keyfind, valfind, origin=None):
self.get_channel_obj(keyfind, valfind).set_status(updatedict) chan_obj = self.get_channel_obj(keyfind, valfind, origin)
if chan_obj:
return chan_obj.dict
return None
def set_channel_enablement_all(self, enablement): def set_channel_status(self, keyfind, valfind, updatedict, origin):
for fhdhr_id in list(self.list.keys()): self.get_channel_obj(keyfind, valfind, origin).set_status(updatedict)
self.list[fhdhr_id].set_enablement(enablement)
def set_channel_enablement(self, keyfind, valfind, enablement): def set_channel_enablement_all(self, enablement, origin):
self.get_channel_obj(keyfind, valfind).set_enablement(enablement) for fhdhr_id in [x["id"] for x in self.get_channels(origin)]:
self.list[fhdhr_id].set_enablement(enablement, origin)
def set_channel_favorite(self, keyfind, valfind, enablement): def set_channel_enablement(self, keyfind, valfind, enablement, origin):
self.get_channel_obj(keyfind, valfind).set_favorite(enablement) self.get_channel_obj(keyfind, valfind, origin).set_enablement(enablement)
def get_db_channels(self): def set_channel_favorite(self, keyfind, valfind, enablement, origin):
self.fhdhr.logger.info("Checking for Channel information stored in the database.") self.get_channel_obj(keyfind, valfind, origin).set_favorite(enablement)
channel_ids = self.fhdhr.db.get_fhdhr_value("channels", "list") or []
def get_db_channels(self, origin=None):
if not origin:
origins_list = list(self.list.keys())
else:
origins_list = origin.lower()
if isinstance(origins_list, str):
origins_list = [origins_list]
for origin in origins_list:
self.fhdhr.logger.info("Checking for %s Channel information stored in the database." % origin)
channel_ids = self.fhdhr.db.get_fhdhr_value("channels", "list", origin) or []
if len(channel_ids): if len(channel_ids):
self.fhdhr.logger.info("Found %s existing channels in the database." % str(len(channel_ids))) self.fhdhr.logger.info("Found %s existing channels in the database." % str(len(channel_ids)))
for channel_id in channel_ids: for channel_id in channel_ids:
channel_obj = Channel(self.fhdhr, self.id_system, channel_id=channel_id) channel_obj = Channel(self.fhdhr, self.id_system, origin=origin, channel_id=channel_id)
channel_id = channel_obj.dict["id"] channel_id = channel_obj.dict["id"]
self.list[channel_id] = channel_obj self.list[origin][channel_id] = channel_obj
def get_channels(self, forceupdate=False): def save_db_channels(self, origin=None):
if not origin:
origins_list = list(self.list.keys())
else:
origins_list = origin.lower()
if isinstance(origins_list, str):
origins_list = [origins_list]
for origin in origins_list:
channel_ids = [self.list[origin][x].dict["id"] for x in list(self.list[origin].keys())]
self.fhdhr.db.set_fhdhr_value("channels", "list", channel_ids, origin)
def get_channels(self, origin=None, forceupdate=False):
"""Pull Channels from origin. """Pull Channels from origin.
Output a list. Output a list.
@ -61,54 +120,64 @@ class Channels():
Don't pull more often than 12 hours. Don't pull more often than 12 hours.
""" """
updatelist = False if not origin:
if not self.list_update_time: origins_list = list(self.list.keys())
updatelist = True else:
elif hours_between_datetime(self.list_update_time, datetime.datetime.now()) > 12: origins_list = origin.lower().lower()
updatelist = True
elif forceupdate:
updatelist = True
if updatelist: if isinstance(origins_list, str):
channel_origin_id_list = [str(self.list[x].dict["origin_id"]) for x in list(self.list.keys())] origins_list = [origins_list]
self.fhdhr.logger.info("Performing Channel Scan.") return_chan_list = []
for origin in origins_list:
channel_dict_list = self.origin.get_channels() if not len(list(self.list[origin].keys())):
self.fhdhr.logger.info("Found %s channels for %s." % (len(channel_dict_list), self.fhdhr.config.dict["main"]["servicename"])) self.get_db_channels(origin=origin)
if not forceupdate:
return_chan_list.extend([self.list[origin][x].dict for x in list(self.list[origin].keys())])
else:
channel_origin_id_list = [str(self.list[origin][x].dict["origin_id"]) for x in list(self.list[origin].keys())]
self.fhdhr.logger.info("Performing Channel Scan for %s." % origin)
channel_dict_list = self.origins.origins_dict[origin].get_channels()
self.fhdhr.logger.info("Found %s channels for %s." % (len(channel_dict_list), origin))
self.fhdhr.logger.info("Performing Channel Import, This can take some time, Please wait.")
newchan = 0 newchan = 0
chan_scan_start = time.time()
for channel_info in channel_dict_list: for channel_info in channel_dict_list:
chan_existing = False chan_existing = str(channel_info["id"]) in channel_origin_id_list
if str(channel_info["id"]) in channel_origin_id_list:
chan_existing = True if chan_existing:
channel_obj = self.get_channel_obj("origin_id", channel_info["id"]) channel_obj = self.get_channel_obj("origin_id", channel_info["id"], origin)
else: else:
channel_obj = Channel(self.fhdhr, self.id_system, origin_id=channel_info["id"]) channel_obj = Channel(self.fhdhr, self.id_system, origin, origin_id=channel_info["id"])
channel_id = channel_obj.dict["id"] channel_id = channel_obj.dict["id"]
channel_obj.basics(channel_info) channel_obj.basics(channel_info)
if not chan_existing: if not chan_existing:
self.list[channel_id] = channel_obj self.list[origin][channel_id] = channel_obj
newchan += 1 newchan += 1
self.fhdhr.logger.info("%s Channel Import took %s" % (origin, humanized_time(time.time() - chan_scan_start)))
if not newchan: if not newchan:
newchan = "no" newchan = "no"
self.fhdhr.logger.info("Found %s NEW channels." % newchan) self.fhdhr.logger.info("Found %s NEW channels for %s." % (newchan, origin))
self.fhdhr.logger.info("Total Channel Count: %s" % len(self.list.keys())) self.fhdhr.logger.info("Total %s Channel Count: %s" % (origin, len(self.list[origin].keys())))
self.save_db_channels(origin=origin)
self.list_update_time = datetime.datetime.now() self.fhdhr.db.set_fhdhr_value("channels", "scanned_time", time.time(), origin)
self.fhdhr.db.set_fhdhr_value("channels", "scanned_time", time.time()) return_chan_list.extend([self.list[origin][x].dict for x in list(self.list[origin].keys())])
channel_list = [] return return_chan_list
for chan_obj in list(self.list.keys()):
channel_list.append(self.list[chan_obj].dict)
return channel_list
def get_channel_stream(self, channel_number): def get_channel_stream(self, stream_args, origin):
return self.origin.get_channel_stream(self.get_channel_dict("number", channel_number)) return self.origins.origins_dict[origin].get_channel_stream(self.get_channel_dict("number", stream_args["channel"]), stream_args)
def get_channel_dict(self, keyfind, valfind):
return self.get_channel_obj(keyfind, valfind).dict

View File

@ -2,36 +2,44 @@ import uuid
class Channel_IDs(): class Channel_IDs():
def __init__(self, fhdhr): def __init__(self, fhdhr, origins):
self.fhdhr = fhdhr self.fhdhr = fhdhr
self.origins = origins
def get(self, origin_id): def get(self, origin_id, origin):
existing_ids = self.fhdhr.db.get_fhdhr_value("channels", "list") or [] existing_ids = self.fhdhr.db.get_fhdhr_value("channels", "list", origin) or []
existing_channel_info = [self.fhdhr.db.get_channel_value(channel_id, "dict") or {} for channel_id in existing_ids] existing_channel_info = [self.fhdhr.db.get_fhdhr_value(channel_id, "dict", origin) or {} for channel_id in existing_ids]
for existing_channel in existing_channel_info: for existing_channel in existing_channel_info:
if existing_channel["origin_id"] == origin_id: if existing_channel["origin_id"] == origin_id:
return existing_channel["id"] return existing_channel["id"]
return self.assign() return self.assign(origin)
def assign(self): def assign(self, origin):
existing_ids = self.fhdhr.db.get_fhdhr_value("channels", "list") or [] existing_ids = self.fhdhr.db.get_fhdhr_value("channels", "list", origin) or []
channel_id = None channel_id = None
while not channel_id: while not channel_id:
unique_id = str(uuid.uuid4()) unique_id = str(uuid.uuid4())
if str(unique_id) not in existing_ids: if str(unique_id) not in existing_ids:
channel_id = str(unique_id) channel_id = str(unique_id)
existing_ids.append(channel_id) existing_ids.append(channel_id)
self.fhdhr.db.set_fhdhr_value("channels", "list", existing_ids) self.fhdhr.db.set_fhdhr_value("channels", "list", existing_ids, origin)
return channel_id return channel_id
def get_number(self, channel_id): def get_number(self, channel_id, origin):
existing_ids = self.fhdhr.db.get_fhdhr_value("channels", "list") or [] existing_ids = self.fhdhr.db.get_fhdhr_value("channels", "list", origin) or []
existing_channel_info = [self.fhdhr.db.get_channel_value(channel_id, "dict") or {} for channel_id in existing_ids] existing_channel_info = [self.fhdhr.db.get_fhdhr_value(channel_id, "dict", origin) or {} for channel_id in existing_ids]
cnumber = [existing_channel["number"] for existing_channel in existing_channel_info if existing_channel["id"] == channel_id] or None cnumber = [existing_channel["number"] for existing_channel in existing_channel_info if existing_channel["id"] == channel_id] or None
if cnumber: if cnumber:
return cnumber return cnumber
used_numbers = [existing_channel["number"] for existing_channel in existing_channel_info] used_numbers = []
for existing_channel in existing_channel_info:
if existing_channel["subnumber"]:
number = "%s.%s" % (existing_channel["number"], existing_channel["subnumber"])
else:
number = existing_channel["number"]
used_numbers.append(number)
for i in range(1000, 2000): for i in range(1000, 2000):
if str(float(i)) not in used_numbers: if str(float(i)) not in used_numbers:
break break

View File

@ -3,21 +3,30 @@ import time
class Channel(): class Channel():
def __init__(self, fhdhr, id_system, origin_id=None, channel_id=None): def __init__(self, fhdhr, id_system, origin, origin_id=None, channel_id=None):
self.fhdhr = fhdhr self.fhdhr = fhdhr
self.origin = origin
self.id_system = id_system self.id_system = id_system
if not channel_id: if not channel_id:
if origin_id: if origin_id:
channel_id = id_system.get(origin_id) channel_id = id_system.get(origin_id, origin)
else: else:
channel_id = id_system.assign() channel_id = id_system.assign(origin)
self.channel_id = channel_id self.channel_id = channel_id
self.dict = self.fhdhr.db.get_channel_value(str(channel_id), "dict") or self.default_dict
self.dict = self.fhdhr.db.get_fhdhr_value(str(channel_id), "dict", self.origin) or self.default_dict
self.verify_dict() self.verify_dict()
self.fhdhr.db.set_channel_value(self.dict["id"], "dict", self.dict) self.fhdhr.db.set_fhdhr_value(self.dict["id"], "dict", self.dict, self.origin)
@property
def number(self):
if self.dict["subnumber"]:
return "%s.%s" % (self.dict["number"], self.dict["subnumber"])
else:
return self.dict["number"]
@property @property
def thumbnail(self): def thumbnail(self):
@ -35,9 +44,9 @@ class Channel():
return { return {
"callsign": self.dict["callsign"], "callsign": self.dict["callsign"],
"name": self.dict["name"], "name": self.dict["name"],
"number": self.dict["number"], "number": self.number,
"id": self.dict["origin_id"], "id": self.dict["origin_id"],
"thumbnail": self.dict["thumbnail"], "thumbnail": self.thumbnail,
"listing": [], "listing": [],
} }
@ -49,37 +58,59 @@ class Channel():
for key in list(default_dict.keys()): for key in list(default_dict.keys()):
if key not in list(self.dict.keys()): if key not in list(self.dict.keys()):
self.dict[key] = default_dict[key] self.dict[key] = default_dict[key]
if self.dict["number"]:
if "." in self.dict["number"]:
self.dict["subnumber"] = self.dict["number"].split(".")[1]
self.dict["number"] = self.dict["number"].split(".")[0]
def basics(self, channel_info): def basics(self, channel_info):
"""Some Channel Information is Critical""" """Some Channel Information is Critical"""
if "name" not in list(channel_info.keys()): if "name" not in list(channel_info.keys()):
channel_info["name"] = self.dict["id"] channel_info["name"] = self.dict["id"]
elif not channel_info["name"]:
channel_info["name"] = self.dict["id"]
self.dict["origin_name"] = channel_info["name"] self.dict["origin_name"] = channel_info["name"]
if not self.dict["name"]: if not self.dict["name"]:
self.dict["name"] = self.dict["origin_name"] self.dict["name"] = self.dict["origin_name"]
if "id" not in list(channel_info.keys()): if "id" not in list(channel_info.keys()):
channel_info["id"] = channel_info["name"] channel_info["id"] = channel_info["name"]
elif not channel_info["id"]:
channel_info["id"] = channel_info["name"]
self.dict["origin_id"] = channel_info["id"] self.dict["origin_id"] = channel_info["id"]
if "callsign" not in list(channel_info.keys()): if "callsign" not in list(channel_info.keys()):
channel_info["callsign"] = channel_info["name"] channel_info["callsign"] = channel_info["name"]
elif not channel_info["callsign"]:
channel_info["callsign"] = channel_info["name"]
self.dict["origin_callsign"] = channel_info["callsign"] self.dict["origin_callsign"] = channel_info["callsign"]
if not self.dict["callsign"]: if not self.dict["callsign"]:
self.dict["callsign"] = self.dict["origin_callsign"] self.dict["callsign"] = self.dict["origin_callsign"]
if "tags" not in list(channel_info.keys()): if "tags" not in list(channel_info.keys()):
channel_info["tags"] = [] channel_info["tags"] = []
elif not channel_info["tags"]:
channel_info["tags"] = []
self.dict["origin_tags"] = channel_info["tags"] self.dict["origin_tags"] = channel_info["tags"]
if not self.dict["tags"]: if not self.dict["tags"]:
self.dict["tags"] = self.dict["origin_tags"] self.dict["tags"] = self.dict["origin_tags"]
if "number" not in list(channel_info.keys()): if "number" not in list(channel_info.keys()):
channel_info["number"] = self.id_system.get_number(channel_info["id"]) channel_info["number"] = self.id_system.get_number(channel_info["id"], self.origin)
self.dict["origin_number"] = str(float(channel_info["number"])) elif not channel_info["number"]:
channel_info["number"] = self.id_system.get_number(channel_info["id"], self.origin)
self.dict["origin_number"] = str(channel_info["number"])
if not self.dict["number"]: if not self.dict["number"]:
self.dict["number"] = self.dict["origin_number"] self.dict["number"] = self.dict["origin_number"].split(".")[0]
try:
self.dict["subnumber"] = self.dict["origin_number"].split(".")[1]
except IndexError:
self.dict["subnumber"] = None
else:
if "." in self.dict["number"]:
self.dict["subnumber"] = self.dict["number"].split(".")[1]
self.dict["number"] = self.dict["number"].split(".")[0]
if "thumbnail" not in list(channel_info.keys()): if "thumbnail" not in list(channel_info.keys()):
channel_info["thumbnail"] = None channel_info["thumbnail"] = None
@ -98,7 +129,7 @@ class Channel():
if "created" not in list(self.dict.keys()): if "created" not in list(self.dict.keys()):
self.dict["created"] = time.time() self.dict["created"] = time.time()
self.fhdhr.db.set_channel_value(self.dict["id"], "dict", self.dict) self.fhdhr.db.set_fhdhr_value(self.dict["id"], "dict", self.dict, self.origin)
@property @property
def default_dict(self): def default_dict(self):
@ -106,7 +137,7 @@ class Channel():
"id": str(self.channel_id), "origin_id": None, "id": str(self.channel_id), "origin_id": None,
"name": None, "origin_name": None, "name": None, "origin_name": None,
"callsign": None, "origin_callsign": None, "callsign": None, "origin_callsign": None,
"number": None, "origin_number": None, "number": None, "subnumber": None, "origin_number": None,
"tags": [], "origin_tags": [], "tags": [], "origin_tags": [],
"thumbnail": None, "origin_thumbnail": None, "thumbnail": None, "origin_thumbnail": None,
"enabled": True, "favorite": 0, "enabled": True, "favorite": 0,
@ -114,48 +145,37 @@ class Channel():
} }
def destroy(self): def destroy(self):
self.fhdhr.db.delete_channel_value(self.dict["id"], "dict") self.fhdhr.db.delete_fhdhr_value(self.dict["id"], "dict", self.origin)
channel_ids = self.fhdhr.db.get_fhdhr_value("channels", "list") or [] channel_ids = self.fhdhr.db.get_fhdhr_value("channels", "list") or []
if self.dict["id"] in channel_ids: if self.dict["id"] in channel_ids:
channel_ids.remove(self.dict["id"]) channel_ids.remove(self.dict["id"])
self.fhdhr.db.set_fhdhr_value("channels", "list", channel_ids) self.fhdhr.db.set_fhdhr_value("channels", "list", channel_ids, self.origin)
def set_status(self, updatedict): def set_status(self, updatedict):
for key in list(updatedict.keys()): for key in list(updatedict.keys()):
if key == "number": if key == "number":
updatedict[key] = str(float(updatedict[key])) updatedict[key] = str(updatedict[key])
self.dict[key] = updatedict[key] self.dict[key] = updatedict[key]
self.fhdhr.db.set_channel_value(self.dict["id"], "dict", self.dict) self.fhdhr.db.set_fhdhr_value(self.dict["id"], "dict", self.dict, self.origin)
@property
def lineup_dict(self):
return {
'GuideNumber': self.dict['number'],
'GuideName': self.dict['name'],
'Tags': ",".join(self.dict['tags']),
'URL': self.stream_url,
'HD': self.dict["HD"],
"Favorite": self.dict["favorite"],
}
@property @property
def generic_image_url(self): def generic_image_url(self):
return "/api/images?method=generate&type=channel&message=%s" % self.dict["number"] return "/api/images?method=generate&type=channel&message=%s" % self.number
@property @property
def stream_url(self): def api_stream_url(self):
return '/auto/v%s' % self.dict['number'] return '/api/tuners?method=stream&stream_method=%s&channel=%s&origin=%s' % (self.fhdhr.origins.origins_dict[self.origin].stream_method, self.dict["id"], self.origin)
@property @property
def play_url(self): def api_m3u_url(self):
return '/api/m3u?method=get&channel=%s' % self.dict['number'] return '/api/m3u?method=get&channel=%s&origin=%s' % (self.dict["id"], self.origin)
def set_favorite(self, enablement): def set_favorite(self, enablement):
if enablement == "+": if enablement == "+":
self.dict["favorite"] = 1 self.dict["favorite"] = 1
elif enablement == "+": elif enablement == "-":
self.dict["favorite"] = 0 self.dict["favorite"] = 0
self.fhdhr.db.set_channel_value(self.dict["id"], "info", self.dict) self.fhdhr.db.set_fhdhr_value(self.dict["id"], "info", self.dict, self.origin)
def set_enablement(self, enablement): def set_enablement(self, enablement):
if enablement == "disable": if enablement == "disable":
@ -167,7 +187,7 @@ class Channel():
self.dict["enabled"] = False self.dict["enabled"] = False
else: else:
self.dict["enabled"] = True self.dict["enabled"] = True
self.fhdhr.db.set_channel_value(self.dict["id"], "info", self.dict) self.fhdhr.db.set_fhdhr_value(self.dict["id"], "info", self.dict, self.origin)
def __getattr__(self, name): def __getattr__(self, name):
''' will only get called for undefined attributes ''' ''' will only get called for undefined attributes '''

View File

@ -1,158 +0,0 @@
from collections import OrderedDict
class fHDHR_Cluster():
def __init__(self, fhdhr, ssdp):
self.fhdhr = fhdhr
self.ssdp = ssdp
self.friendlyname = self.fhdhr.config.dict["fhdhr"]["friendlyname"]
if self.fhdhr.config.dict["fhdhr"]["discovery_address"]:
self.startup_sync()
def cluster(self):
return self.fhdhr.db.get_fhdhr_value("cluster", "dict") or self.default_cluster()
def get_cluster_dicts_web(self):
fhdhr_list = self.cluster()
locations = []
for location in list(fhdhr_list.keys()):
item_dict = {
"base_url": fhdhr_list[location]["base_url"],
"name": fhdhr_list[location]["name"]
}
if item_dict["base_url"] != self.fhdhr.api.base:
locations.append(item_dict)
if len(locations):
locations = sorted(locations, key=lambda i: i['name'])
return locations
else:
return None
def get_list(self):
cluster = self.fhdhr.db.get_fhdhr_value("cluster", "dict") or self.default_cluster()
return_dict = {}
for location in list(cluster.keys()):
if location != self.fhdhr.api.base:
return_dict[location] = {
"Joined": True
}
detected_list = self.ssdp.detect_method.get()
for location in detected_list:
if location not in list(cluster.keys()):
return_dict[location] = {
"Joined": False
}
return_dict = OrderedDict(sorted(return_dict.items()))
return return_dict
def default_cluster(self):
defdict = {}
defdict[self.fhdhr.api.base] = {
"base_url": self.fhdhr.api.base,
"name": self.friendlyname
}
return defdict
def startup_sync(self):
self.fhdhr.logger.info("Syncronizing with Cluster.")
cluster = self.fhdhr.db.get_fhdhr_value("cluster", "dict") or self.default_cluster()
if not len(list(cluster.keys())):
self.fhdhr.logger.info("No Cluster Found.")
else:
self.fhdhr.logger.info("Found %s clustered services." % str(len(list(cluster.keys()))))
for location in list(cluster.keys()):
if location != self.fhdhr.api.base:
self.fhdhr.logger.info("Checking Cluster Syncronization information from %s." % location)
sync_url = location + "/api/cluster?method=get"
try:
sync_open = self.fhdhr.web.session.get(sync_url)
retrieved_cluster = sync_open.json()
if self.fhdhr.api.base not in list(retrieved_cluster.keys()):
return self.leave()
except self.fhdhr.web.exceptions.ConnectionError:
self.fhdhr.logger.error("Unreachable: " + location)
def leave(self):
self.fhdhr.logger.info("Leaving cluster.")
self.fhdhr.db.set_fhdhr_value("cluster", "dict", self.default_cluster())
def disconnect(self):
cluster = self.fhdhr.db.get_fhdhr_value("cluster", "dict") or self.default_cluster()
for location in list(cluster.keys()):
if location != self.fhdhr.api.base:
self.fhdhr.logger.info("Informing %s that I am departing the Cluster." % location)
sync_url = location + "/api/cluster?method=del&location=" + self.fhdhr.api.base
try:
self.fhdhr.web.session.get(sync_url)
except self.fhdhr.web.exceptions.ConnectionError:
self.fhdhr.logger.error("Unreachable: " + location)
self.leave()
def sync(self, location):
sync_url = location + "/api/cluster?method=get"
try:
sync_open = self.fhdhr.web.session.get(sync_url)
self.fhdhr.db.set_fhdhr_value("cluster", "dict", sync_open.json())
except self.fhdhr.web.exceptions.ConnectionError:
self.fhdhr.logger.error("Unreachable: " + location)
def push_sync(self):
cluster = self.fhdhr.db.get_fhdhr_value("cluster", "dict") or self.default_cluster()
for location in list(cluster.keys()):
if location != self.fhdhr.api.base:
sync_url = location + "/api/cluster?method=sync&location=" + self.fhdhr.api.base_quoted
try:
self.fhdhr.web.session.get(sync_url)
except self.fhdhr.web.exceptions.ConnectionError:
self.fhdhr.logger.error("Unreachable: " + location)
def add(self, location):
cluster = self.fhdhr.db.get_fhdhr_value("cluster", "dict") or self.default_cluster()
if location not in list(cluster.keys()):
self.fhdhr.logger.info("Adding %s to cluster." % location)
cluster[location] = {"base_url": location}
location_info_url = "%s/hdhr/discover.json" % location
try:
location_info_req = self.fhdhr.web.session.get(location_info_url)
except self.fhdhr.web.exceptions.ConnectionError:
self.fhdhr.logger.error("Unreachable: " + location)
del cluster[location]
self.fhdhr.db.set_fhdhr_value("cluster", "dict", cluster)
return
location_info = location_info_req.json()
cluster[location]["name"] = location_info["FriendlyName"]
cluster_info_url = location + "/api/cluster?method=get"
try:
cluster_info_req = self.fhdhr.web.session.get(cluster_info_url)
except self.fhdhr.web.exceptions.ConnectionError:
self.fhdhr.logger.error("Unreachable: " + location)
del cluster[location]
self.fhdhr.db.set_fhdhr_value("cluster", "dict", cluster)
return
cluster_info = cluster_info_req.json()
for cluster_key in list(cluster_info.keys()):
if cluster_key not in list(cluster.keys()):
cluster[cluster_key] = cluster_info[cluster_key]
self.fhdhr.db.set_fhdhr_value("cluster", "dict", cluster)
self.push_sync()
def remove(self, location):
cluster = self.fhdhr.db.get_fhdhr_value("cluster", "dict") or self.default_cluster()
if location in list(cluster.keys()):
self.fhdhr.logger.info("Removing %s from cluster." % location)
del cluster[location]
sync_url = location + "/api/cluster?method=leave"
try:
self.fhdhr.web.session.get(sync_url)
except self.fhdhr.web.exceptions.ConnectionError:
self.fhdhr.logger.error("Unreachable: " + location)
self.push_sync()
self.fhdhr.db.set_fhdhr_value("cluster", "dict", cluster)

View File

@ -1,30 +1,27 @@
import os
import time import time
import datetime import datetime
from collections import OrderedDict import threading
from fHDHR.tools import channel_sort
from .blocks import blocksEPG from .blocks import blocksEPG
class EPG(): class EPG():
def __init__(self, fhdhr, channels, originwrapper, alternative_epg): def __init__(self, fhdhr, channels, origins):
self.fhdhr = fhdhr self.fhdhr = fhdhr
self.origin = originwrapper self.origins = origins
self.channels = channels self.channels = channels
self.alternative_epg = alternative_epg
self.epgdict = {} self.epgdict = {}
self.epg_methods = self.fhdhr.config.dict["epg"]["method"] self.epg_methods = self.fhdhr.config.dict["epg"]["method"] or []
self.valid_epg_methods = [x for x in self.fhdhr.config.dict["main"]["valid_epg_methods"] if x and x not in [None, "None"]] self.valid_epg_methods = [x for x in list(self.fhdhr.config.dict["epg"]["valid_methods"].keys()) if x and x not in [None, "None"]]
self.blocks = blocksEPG(self.fhdhr, self.channels) self.blocks = blocksEPG(self.fhdhr, self.channels, self.origins, None)
self.epg_handling = { self.epg_handling = {}
"origin": self.origin,
"blocks": self.blocks,
}
self.epg_method_selfadd() self.epg_method_selfadd()
self.def_method = self.fhdhr.config.dict["epg"]["def_method"] self.def_method = self.fhdhr.config.dict["epg"]["def_method"]
@ -36,21 +33,21 @@ class EPG():
if epg_method not in list(self.sleeptime.keys()): if epg_method not in list(self.sleeptime.keys()):
self.sleeptime[epg_method] = self.fhdhr.config.dict["epg"]["update_frequency"] self.sleeptime[epg_method] = self.fhdhr.config.dict["epg"]["update_frequency"]
self.epg_update_url = "%s/api/epg?method=update" % (self.fhdhr.api.base) self.epg_update_url = "/api/epg?method=update"
self.fhdhr.threads["epg"] = threading.Thread(target=self.run)
def clear_epg_cache(self, method=None): def clear_epg_cache(self, method=None):
if not method: if not method:
if not self.def_method:
return
if method not in self.valid_epg_methods:
if not self.def_method:
return
method = self.def_method method = self.def_method
if (method == self.fhdhr.config.dict["main"]["dictpopname"] or
method not in self.fhdhr.config.dict["main"]["valid_epg_methods"]):
method = "origin"
epgtypename = method self.fhdhr.logger.info("Clearing %s EPG cache." % method)
if method in [self.fhdhr.config.dict["main"]["dictpopname"], "origin"]:
epgtypename = self.fhdhr.config.dict["main"]["dictpopname"]
self.fhdhr.logger.info("Clearing " + epgtypename + " EPG cache.")
if hasattr(self.epg_handling[method], 'clear_cache'): if hasattr(self.epg_handling[method], 'clear_cache'):
self.epg_handling[method].clear_cache() self.epg_handling[method].clear_cache()
@ -60,54 +57,84 @@ class EPG():
self.fhdhr.db.delete_fhdhr_value("epg_dict", method) self.fhdhr.db.delete_fhdhr_value("epg_dict", method)
def whats_on_now(self, channel, method=None): def whats_on_now(self, channel_number, method=None, chan_obj=None, chan_dict=None):
nowtime = time.time()
epgdict = self.get_epg(method) epgdict = self.get_epg(method)
listings = epgdict[channel]["listing"] if channel_number not in list(epgdict.keys()):
for listing in listings: epgdict[channel_number] = {
nowtime = datetime.datetime.utcnow() "callsign": "",
start_time = datetime.datetime.strptime(listing["time_start"], '%Y%m%d%H%M%S +0000') "name": "",
end_time = datetime.datetime.strptime(listing["time_end"], '%Y%m%d%H%M%S +0000') "number": str(channel_number),
if start_time <= nowtime <= end_time: "id": "",
epgitem = epgdict[channel].copy() "thumbnail": "",
"listing": []
}
for listing in epgdict[channel_number]["listing"]:
for time_item in ["time_start", "time_end"]:
time_value = listing[time_item]
if str(time_value).endswith("+00:00"):
listing[time_item] = datetime.datetime.strptime(time_value, '%Y%m%d%H%M%S +00:00').timestamp()
elif str(time_value).endswith("+0000"):
listing[time_item] = datetime.datetime.strptime(time_value, '%Y%m%d%H%M%S +0000').timestamp()
else:
listing[time_item] = int(time_value)
if int(listing["time_start"]) <= nowtime <= int(listing["time_end"]):
epgitem = epgdict[channel_number].copy()
epgitem["listing"] = [listing] epgitem["listing"] = [listing]
return epgitem return epgitem
return None epgitem = epgdict[channel_number].copy()
epgitem["listing"] = [self.blocks.empty_listing(chan_obj=None, chan_dict=None)]
return epgitem
def whats_on_allchans(self, method=None): def whats_on_allchans(self, method=None):
if not method: if not method:
if not self.def_method:
return
method = self.def_method
if method not in self.valid_epg_methods:
if not self.def_method:
return
method = self.def_method method = self.def_method
if (method == self.fhdhr.config.dict["main"]["dictpopname"] or
method not in self.fhdhr.config.dict["main"]["valid_epg_methods"]):
method = "origin"
channel_guide_list = [] channel_guide_dict = {}
epgdict = self.get_epg(method) epgdict = self.get_epg(method)
channels = list(epgdict.keys()) epgdict = epgdict.copy()
for channel in channels: for c in list(epgdict.keys()):
whatson = self.whats_on_now(epgdict[channel]["number"], method) if method in [origin for origin in list(self.origins.origins_dict.keys())]:
chan_obj = self.channels.get_channel_obj("origin_id", epgdict[c]["id"])
channel_number = chan_obj.number
epgdict[channel_number] = epgdict.pop(c)
epgdict[channel_number]["name"] = chan_obj.dict["name"]
epgdict[channel_number]["callsign"] = chan_obj.dict["callsign"]
epgdict[channel_number]["number"] = chan_obj.number
epgdict[channel_number]["id"] = chan_obj.dict["origin_id"]
epgdict[channel_number]["thumbnail"] = chan_obj.thumbnail
else:
chan_obj = None
channel_number = c
whatson = self.whats_on_now(channel_number, method, chan_dict=epgdict, chan_obj=chan_obj)
if whatson: if whatson:
channel_guide_list.append(whatson) channel_guide_dict[channel_number] = whatson
return channel_guide_list return channel_guide_dict
def get_epg(self, method=None): def get_epg(self, method=None):
if not method: if not method:
if not self.def_method:
return
method = self.def_method
if method not in self.valid_epg_methods:
if not self.def_method:
return
method = self.def_method method = self.def_method
if (method == self.fhdhr.config.dict["main"]["dictpopname"] or
method not in self.fhdhr.config.dict["main"]["valid_epg_methods"]):
method = "origin"
if method not in list(self.epgdict.keys()): if method in list(self.epgdict.keys()):
return self.epgdict[method]
epgdict = self.fhdhr.db.get_fhdhr_value("epg_dict", method) or None
if not epgdict:
self.update(method) self.update(method)
self.epgdict[method] = self.fhdhr.db.get_fhdhr_value("epg_dict", method) or {} self.epgdict[method] = self.fhdhr.db.get_fhdhr_value("epg_dict", method) or {}
else:
self.epgdict[method] = epgdict
return self.epgdict[method]
else:
return self.epgdict[method] return self.epgdict[method]
def get_thumbnail(self, itemtype, itemid): def get_thumbnail(self, itemtype, itemid):
@ -121,71 +148,171 @@ class EPG():
def find_channel_dict(self, channel_id): def find_channel_dict(self, channel_id):
epgdict = self.get_epg() epgdict = self.get_epg()
channel_list = [] channel_list = [epgdict[x] for x in list(epgdict.keys())]
for channel in list(epgdict.keys()): return next(item for item in channel_list if item["id"] == channel_id) or None
channel_list.append(epgdict[channel])
return next(item for item in channel_list if item["id"] == channel_id)
def find_program_dict(self, event_id): def find_program_dict(self, event_id):
epgdict = self.get_epg() epgdict = self.get_epg()
event_list = [] event_list = []
for channel in list(epgdict.keys()): for channel in list(epgdict.keys()):
event_list.extend(epgdict[channel]["listing"]) event_list.extend(epgdict[channel]["listing"])
return next(item for item in event_list if item["id"] == event_id) return next(item for item in event_list if item["id"] == event_id) or None
def epg_method_selfadd(self): def epg_method_selfadd(self):
self.fhdhr.logger.info("Checking for Alternative EPG methods.") for plugin_name in list(self.fhdhr.plugins.plugins.keys()):
new_epgtype_list = [] if self.fhdhr.plugins.plugins[plugin_name].type == "alt_epg":
for entry in os.scandir(self.fhdhr.config.internal["paths"]["alternative_epg"]): method = self.fhdhr.plugins.plugins[plugin_name].name.lower()
if entry.is_file(): self.epg_handling[method] = self.fhdhr.plugins.plugins[plugin_name].Plugin_OBJ(self.channels, self.fhdhr.plugins.plugins[plugin_name].plugin_utils)
if entry.name[0] != '_' and entry.name.endswith(".py"): for origin in list(self.origins.origins_dict.keys()):
new_epgtype_list.append(str(entry.name[:-3])) if origin.lower() not in list(self.epg_handling.keys()):
for method in new_epgtype_list: self.epg_handling[origin.lower()] = blocksEPG(self.fhdhr, self.channels, self.origins, origin)
self.fhdhr.logger.info("Found %s EPG method." % method) self.fhdhr.config.register_valid_epg_method(origin, "Blocks")
self.epg_handling[method] = eval("self.alternative_epg.%s.%sEPG(self.fhdhr, self.channels)" % (method, method)) self.valid_epg_methods.append(origin.lower())
def update(self, method=None): def update(self, method=None):
if (not method or if not method:
method not in self.fhdhr.config.dict["main"]["valid_epg_methods"]): if not self.def_method:
return
method = self.def_method
if method not in self.valid_epg_methods:
if not self.def_method:
return
method = self.def_method method = self.def_method
if method == self.fhdhr.config.dict["main"]["dictpopname"]: self.fhdhr.logger.info("Updating %s EPG cache." % method)
method = "origin"
epgtypename = method
if method in [self.fhdhr.config.dict["main"]["dictpopname"], "origin"]:
epgtypename = self.fhdhr.config.dict["main"]["dictpopname"]
self.fhdhr.logger.info("Updating " + epgtypename + " EPG cache.")
if method == 'origin':
programguide = self.epg_handling['origin'].update_epg(self.channels)
else:
programguide = self.epg_handling[method].update_epg() programguide = self.epg_handling[method].update_epg()
for chan in list(programguide.keys()): # sort the channel listings by time stamp
floatnum = str(float(chan)) for cnum in list(programguide.keys()):
programguide[floatnum] = programguide.pop(chan)
programguide[floatnum]["number"] = floatnum
programguide = OrderedDict(sorted(programguide.items()))
for cnum in programguide:
programguide[cnum]["listing"] = sorted(programguide[cnum]["listing"], key=lambda i: i['time_start']) programguide[cnum]["listing"] = sorted(programguide[cnum]["listing"], key=lambda i: i['time_start'])
self.epgdict = programguide # Gernate Block periods for between EPG data, if missing
clean_prog_guide = {}
desired_start_time = (datetime.datetime.today() + datetime.timedelta(days=self.fhdhr.config.dict["epg"]["reverse_days"])).timestamp()
desired_end_time = (datetime.datetime.today() + datetime.timedelta(days=self.fhdhr.config.dict["epg"]["forward_days"])).timestamp()
for cnum in list(programguide.keys()):
if cnum not in list(clean_prog_guide.keys()):
clean_prog_guide[cnum] = programguide[cnum].copy()
clean_prog_guide[cnum]["listing"] = []
if method in [origin for origin in list(self.origins.origins_dict.keys())]:
chan_obj = self.channels.get_channel_obj("origin_id", programguide[cnum]["id"])
else:
chan_obj = None
# Generate Blocks for Channels containing No Lisiings
if not len(programguide[cnum]["listing"]):
timestamps = self.blocks.timestamps_between(desired_start_time, desired_end_time)
clean_prog_dicts = self.blocks.empty_channel_epg(timestamps, chan_dict=programguide[cnum], chan_obj=chan_obj)
clean_prog_guide[cnum]["listing"].extend(clean_prog_dicts)
else:
# Clean Timetamps from old xmltv method to timestamps
progindex = 0
for program_item in programguide[cnum]["listing"]:
for time_item in ["time_start", "time_end"]:
time_value = programguide[cnum]["listing"][progindex][time_item]
if str(time_value).endswith("+00:00"):
programguide[cnum]["listing"][progindex][time_item] = datetime.datetime.strptime(time_value, '%Y%m%d%H%M%S +00:00').timestamp()
elif str(time_value).endswith("+0000"):
programguide[cnum]["listing"][progindex][time_item] = datetime.datetime.strptime(time_value, '%Y%m%d%H%M%S +0000').timestamp()
else:
programguide[cnum]["listing"][progindex][time_item] = int(time_value)
progindex += 1
# Generate time before the listing actually starts
first_prog_time = programguide[cnum]["listing"][0]['time_start']
if desired_start_time < first_prog_time:
timestamps = self.blocks.timestamps_between(desired_start_time, first_prog_time)
clean_prog_dicts = self.blocks.empty_channel_epg(timestamps, chan_dict=programguide[cnum], chan_obj=chan_obj)
clean_prog_guide[cnum]["listing"].extend(clean_prog_dicts)
# Generate time blocks between events if chunks of time are missing
progindex = 0
for program_item in programguide[cnum]["listing"]:
try:
nextprog_dict = programguide[cnum]["listing"][progindex + 1]
except IndexError:
nextprog_dict = None
if not nextprog_dict:
clean_prog_guide[cnum]["listing"].append(program_item)
else:
if nextprog_dict['time_start'] > program_item['time_end']:
timestamps = self.blocks.timestamps_between(program_item['time_end'], nextprog_dict['time_start'])
clean_prog_dicts = self.blocks.empty_channel_epg(timestamps, chan_dict=programguide[cnum], chan_obj=chan_obj)
clean_prog_guide[cnum]["listing"].extend(clean_prog_dicts)
else:
clean_prog_guide[cnum]["listing"].append(program_item)
progindex += 1
# Generate time after the listing actually ends
end_prog_time = programguide[cnum]["listing"][progindex]['time_end']
if desired_end_time > end_prog_time:
timestamps = self.blocks.timestamps_between(end_prog_time, desired_end_time)
clean_prog_dicts = self.blocks.empty_channel_epg(timestamps, chan_dict=programguide[cnum], chan_obj=chan_obj)
clean_prog_guide[cnum]["listing"].extend(clean_prog_dicts)
programguide = clean_prog_guide.copy()
# if a stock method, generate Blocks EPG for missing channels
if method in [origin for origin in list(self.origins.origins_dict.keys())]:
timestamps = self.blocks.timestamps
for fhdhr_id in [x["id"] for x in self.channels.get_channels(method)]:
chan_obj = self.channels.get_channel_obj("id", fhdhr_id, method)
if str(chan_obj.number) not in list(programguide.keys()):
programguide[str(chan_obj.number)] = chan_obj.epgdict
clean_prog_dicts = self.blocks.empty_channel_epg(timestamps, chan_obj=chan_obj)
programguide[str(chan_obj.number)]["listing"].extend(clean_prog_dicts)
# Make Thumbnails for missing thumbnails
for cnum in list(programguide.keys()):
if not programguide[cnum]["thumbnail"]:
programguide[cnum]["thumbnail"] = "/api/images?method=generate&type=channel&message=%s" % programguide[cnum]["number"]
programguide[cnum]["listing"] = sorted(programguide[cnum]["listing"], key=lambda i: i['time_start'])
prog_index = 0
for program_item in programguide[cnum]["listing"]:
if not programguide[cnum]["listing"][prog_index]["thumbnail"]:
programguide[cnum]["listing"][prog_index]["thumbnail"] = programguide[cnum]["thumbnail"]
prog_index += 1
# Get Totals
total_channels = len(list(programguide.keys()))
total_programs = 0
# Sort the channels
sorted_channel_list = channel_sort(list(programguide.keys()))
sorted_chan_guide = {}
for channel in sorted_channel_list:
total_programs += len(programguide[cnum]["listing"])
sorted_chan_guide[channel] = programguide[channel]
self.epgdict[method] = sorted_chan_guide
self.fhdhr.db.set_fhdhr_value("epg_dict", method, programguide) self.fhdhr.db.set_fhdhr_value("epg_dict", method, programguide)
self.fhdhr.db.set_fhdhr_value("update_time", method, time.time()) self.fhdhr.db.set_fhdhr_value("update_time", method, time.time())
self.fhdhr.logger.info("Wrote " + epgtypename + " EPG cache.") self.fhdhr.logger.info("Wrote %s EPG cache. %s Programs for %s Channels" % (method, total_programs, total_channels))
def start(self):
self.fhdhr.logger.info("EPG Update Thread Starting")
self.fhdhr.threads["epg"].start()
def stop(self):
self.fhdhr.logger.info("EPG Update Thread Stopping")
def run(self): def run(self):
for epg_method in self.epg_methods: time.sleep(1800)
self.fhdhr.web.session.get(self.epg_update_url)
try:
while True: while True:
for epg_method in self.epg_methods: for epg_method in self.epg_methods:
if time.time() >= (self.fhdhr.db.get_fhdhr_value("update_time", epg_method) + self.sleeptime[epg_method]): last_update_time = self.fhdhr.db.get_fhdhr_value("update_time", epg_method)
self.fhdhr.web.session.get(self.epg_update_url) updatetheepg = False
time.sleep(360) if not last_update_time:
except KeyboardInterrupt: updatetheepg = True
pass elif time.time() >= (last_update_time + self.sleeptime[epg_method]):
updatetheepg = True
if updatetheepg:
self.fhdhr.api.get("%s&source=%s" % (self.epg_update_url, epg_method))
time.sleep(1800)
self.stop()

View File

@ -3,61 +3,65 @@ import datetime
class blocksEPG(): class blocksEPG():
def __init__(self, fhdhr, channels): def __init__(self, fhdhr, channels, origins, origin):
self.fhdhr = fhdhr self.fhdhr = fhdhr
self.channels = channels self.channels = channels
self.origins = origins
self.origin = origin
def update_epg(self): def update_epg(self):
programguide = {} programguide = {}
timestamps = self.timestamps timestamps = self.timestamps
for fhdhr_id in list(self.channels.list.keys()): for fhdhr_id in [x["id"] for x in self.channels.get_channels(self.origin)]:
chan_obj = self.channels.list[fhdhr_id] chan_obj = self.channels.get_channel_obj("id", fhdhr_id, self.origin)
if str(chan_obj.dict["number"]) not in list(programguide.keys()): if str(chan_obj.number) not in list(programguide.keys()):
programguide[str(chan_obj.dict["number"])] = chan_obj.epgdict programguide[str(chan_obj.number)] = chan_obj.epgdict
clean_prog_dicts = self.empty_channel_epg(timestamps, chan_obj) clean_prog_dicts = self.empty_channel_epg(timestamps, chan_obj=chan_obj)
for clean_prog_dict in clean_prog_dicts: for clean_prog_dict in clean_prog_dicts:
programguide[str(chan_obj.dict["number"])]["listing"].append(clean_prog_dict) programguide[str(chan_obj.number)]["listing"].append(clean_prog_dict)
return programguide return programguide
def get_content_thumbnail(self, content_id):
return "/api/images?method=generate&type=content&message=%s" % content_id
@property @property
def timestamps(self): def timestamps(self):
timestamps = [] desired_start_time = (datetime.datetime.today() + datetime.timedelta(days=self.fhdhr.config.dict["epg"]["reverse_days"])).timestamp()
todaydate = datetime.date.today() desired_end_time = (datetime.datetime.today() + datetime.timedelta(days=self.fhdhr.config.dict["epg"]["forward_days"])).timestamp()
for x in range(0, 6): return self.timestamps_between(desired_start_time, desired_end_time)
xdate = todaydate + datetime.timedelta(days=x)
xtdate = xdate + datetime.timedelta(days=1)
for hour in range(0, 24): def timestamps_between(self, starttime, endtime):
time_start = datetime.datetime.combine(xdate, datetime.time(hour, 0)) timestamps = []
if hour + 1 < 24: desired_blocksize = self.fhdhr.config.dict["epg"]["block_size"]
time_end = datetime.datetime.combine(xdate, datetime.time(hour + 1, 0)) current_time = starttime
else: while (current_time + desired_blocksize) <= endtime:
time_end = datetime.datetime.combine(xtdate, datetime.time(0, 0))
timestampdict = { timestampdict = {
"time_start": str(time_start.strftime('%Y%m%d%H%M%S')) + " +0000", "time_start": current_time,
"time_end": str(time_end.strftime('%Y%m%d%H%M%S')) + " +0000", "time_end": current_time + desired_blocksize,
}
timestamps.append(timestampdict)
current_time += desired_blocksize
if current_time < endtime:
timestampdict = {
"time_start": current_time,
"time_end": endtime
} }
timestamps.append(timestampdict) timestamps.append(timestampdict)
return timestamps return timestamps
def empty_channel_epg(self, timestamps, chan_obj): def single_channel_epg(self, timestampdict, chan_obj=None, chan_dict=None):
clean_prog_dicts = []
for timestamp in timestamps: if chan_obj:
content_id = "%s_%s" % (chan_obj.dict["origin_id"], str(timestamp['time_start']).split(" ")[0]) content_id = "%s_%s" % (chan_obj.dict["origin_id"], timestampdict['time_start'])
elif chan_dict:
content_id = "%s_%s" % (chan_dict["id"], timestampdict['time_start'])
clean_prog_dict = { clean_prog_dict = {
"time_start": timestamp['time_start'], "time_start": timestampdict['time_start'],
"time_end": timestamp['time_end'], "time_end": timestampdict['time_end'],
"duration_minutes": 60, "duration_minutes": (timestampdict['time_end'] - timestampdict['time_start']) / 60,
"thumbnail": chan_obj.dict["thumbnail"] or self.get_content_thumbnail(content_id),
"title": "Unavailable", "title": "Unavailable",
"sub-title": "Unavailable", "sub-title": "Unavailable",
"description": "Unavailable", "description": "Unavailable",
@ -70,5 +74,47 @@ class blocksEPG():
"isnew": False, "isnew": False,
"id": content_id, "id": content_id,
} }
if chan_obj:
clean_prog_dict["thumbnail"] = chan_obj.thumbnail
elif chan_dict:
clean_prog_dict["thumbnail"] = chan_dict["thumbnail"]
if not clean_prog_dict["thumbnail"]:
clean_prog_dict["thumbnail"] = "/api/images?method=generate&type=content&message=%s" % content_id
return clean_prog_dict
def empty_channel_epg(self, timestamps, chan_obj=None, chan_dict=None):
clean_prog_dicts = []
for timestampdict in timestamps:
clean_prog_dict = self.single_channel_epg(timestampdict, chan_obj=chan_obj, chan_dict=chan_dict)
clean_prog_dicts.append(clean_prog_dict) clean_prog_dicts.append(clean_prog_dict)
return clean_prog_dicts return clean_prog_dicts
def empty_listing(self, chan_obj=None, chan_dict=None):
clean_prog_dict = {
"time_start": None,
"time_end": None,
"duration_minutes": None,
"title": "Unavailable",
"sub-title": "Unavailable",
"description": "Unavailable",
"rating": "N/A",
"episodetitle": None,
"releaseyear": None,
"genres": [],
"seasonnumber": None,
"episodenumber": None,
"isnew": False,
"id": "Unavailable",
}
if chan_obj:
clean_prog_dict["thumbnail"] = chan_obj.thumbnail
elif chan_dict:
clean_prog_dict["thumbnail"] = chan_dict["thumbnail"]
else:
clean_prog_dict["thumbnail"] = None
if not clean_prog_dict["thumbnail"]:
clean_prog_dict["thumbnail"] = "/api/images?method=generate&type=content&message=Unavailable"
return clean_prog_dict

View File

@ -2,10 +2,7 @@
import socket import socket
import struct import struct
import time import time
import threading
from .ssdp_detect import fHDHR_Detect
from .rmg_ssdp import RMG_SSDP
from .hdhr_ssdp import HDHR_SSDP
class SSDPServer(): class SSDPServer():
@ -13,10 +10,14 @@ class SSDPServer():
def __init__(self, fhdhr): def __init__(self, fhdhr):
self.fhdhr = fhdhr self.fhdhr = fhdhr
self.detect_method = fHDHR_Detect(fhdhr) self.ssdp_handling = {}
self.methods = [x for x in list(self.fhdhr.plugins.plugins.keys()) if self.fhdhr.plugins.plugins[x].type == "ssdp"]
if (self.fhdhr.config.dict["fhdhr"]["discovery_address"] and if (self.fhdhr.config.dict["fhdhr"]["discovery_address"] and
self.fhdhr.config.dict["ssdp"]["enabled"]): self.fhdhr.config.dict["ssdp"]["enabled"] and
len(self.methods)):
self.fhdhr.threads["ssdp"] = threading.Thread(target=self.run)
self.setup_ssdp() self.setup_ssdp()
self.sock.bind((self.bind_address, 1900)) self.sock.bind((self.bind_address, 1900))
@ -26,12 +27,33 @@ class SSDPServer():
self.max_age = int(fhdhr.config.dict["ssdp"]["max_age"]) self.max_age = int(fhdhr.config.dict["ssdp"]["max_age"])
self.age_time = None self.age_time = None
self.rmg_ssdp = RMG_SSDP(fhdhr, self.broadcast_ip, self.max_age) self.ssdp_method_selfadd()
self.hdhr_ssdp = HDHR_SSDP(fhdhr, self.broadcast_ip, self.max_age)
self.do_alive() self.do_alive()
self.m_search() self.m_search()
def ssdp_method_selfadd(self):
for plugin_name in list(self.fhdhr.plugins.plugins.keys()):
if self.fhdhr.plugins.plugins[plugin_name].type == "ssdp":
method = self.fhdhr.plugins.plugins[plugin_name].name.lower()
plugin_utils = self.fhdhr.plugins.plugins[plugin_name].plugin_utils
self.ssdp_handling[method] = self.fhdhr.plugins.plugins[plugin_name].Plugin_OBJ(self.fhdhr, plugin_utils, self.broadcast_ip, self.max_age)
def start(self):
self.fhdhr.logger.info("SSDP Server Starting")
self.fhdhr.threads["ssdp"].start()
def stop(self):
self.fhdhr.logger.info("SSDP Server Stopping")
self.sock.close()
def run(self):
while True:
data, address = self.sock.recvfrom(1024)
self.on_recv(data, address)
self.do_alive()
self.stop()
def do_alive(self, forcealive=False): def do_alive(self, forcealive=False):
send_alive = False send_alive = False
@ -44,21 +66,22 @@ class SSDPServer():
if send_alive: if send_alive:
self.fhdhr.logger.info("Sending Alive message to network.") self.fhdhr.logger.info("Sending Alive message to network.")
self.do_notify(self.broadcase_address_tuple) self.do_notify(self.broadcast_address_tuple)
self.age_time = time.time() self.age_time = time.time()
def do_notify(self, address): def do_notify(self, address):
notify_list = [] notify_list = []
for ssdp_handler in list(self.ssdp_handling.keys()):
hdhr_notify = self.hdhr_ssdp.get() if self.ssdp_handling[ssdp_handler].enabled and hasattr(self.ssdp_handling[ssdp_handler], 'notify'):
notify_list.append(hdhr_notify) notify_data = self.ssdp_handling[ssdp_handler].notify
if isinstance(notify_data, list):
if self.fhdhr.config.dict["rmg"]["enabled"]: notify_list.extend(notify_data)
rmg_notify = self.rmg_ssdp.get() else:
notify_list.append(rmg_notify) notify_list.append(notify_data)
for notifydata in notify_list: for notifydata in notify_list:
notifydata = notifydata.encode("utf-8")
self.fhdhr.logger.debug("Created {}".format(notifydata)) self.fhdhr.logger.debug("Created {}".format(notifydata))
try: try:
@ -85,6 +108,10 @@ class SSDPServer():
headers = [x.split(':', 1) for x in lines] headers = [x.split(':', 1) for x in lines]
headers = dict(map(lambda x: (x[0].lower(), x[1]), headers)) headers = dict(map(lambda x: (x[0].lower(), x[1]), headers))
for ssdp_handler in list(self.ssdp_handling.keys()):
if self.ssdp_handling[ssdp_handler].enabled and hasattr(self.ssdp_handling[ssdp_handler], 'on_recv'):
self.ssdp_handling[ssdp_handler].on_recv(headers, cmd, list(self.ssdp_handling.keys()))
if cmd[0] == 'M-SEARCH' and cmd[1] == '*': if cmd[0] == 'M-SEARCH' and cmd[1] == '*':
# SSDP discovery # SSDP discovery
self.fhdhr.logger.debug("Received qualifying M-SEARCH from {}".format(address)) self.fhdhr.logger.debug("Received qualifying M-SEARCH from {}".format(address))
@ -92,26 +119,14 @@ class SSDPServer():
self.do_notify(address) self.do_notify(address)
elif cmd[0] == 'NOTIFY' and cmd[1] == '*': if cmd[0] == 'NOTIFY' and cmd[1] == '*':
# SSDP presence
self.fhdhr.logger.debug("NOTIFY data: {}".format(headers)) self.fhdhr.logger.debug("NOTIFY data: {}".format(headers))
try:
if headers["server"].startswith("fHDHR"):
savelocation = headers["location"].split("/device.xml")[0]
if savelocation.endswith("/hdhr"):
savelocation = savelocation.replace("/hdhr", '')
elif savelocation.endswith("/rmg"):
savelocation = savelocation.replace("/rmg", '')
if savelocation != self.fhdhr.api.base:
self.detect_method.set(savelocation)
except KeyError:
return
else: else:
self.fhdhr.logger.debug('Unknown SSDP command %s %s' % (cmd[0], cmd[1])) self.fhdhr.logger.debug('Unknown SSDP command %s %s' % (cmd[0], cmd[1]))
def m_search(self): def m_search(self):
data = self.msearch_payload data = self.msearch_payload
self.sock.sendto(data, self.broadcase_address_tuple) self.sock.sendto(data, self.broadcast_address_tuple)
def create_msearch_payload(self): def create_msearch_payload(self):
@ -132,15 +147,6 @@ class SSDPServer():
return data.encode("utf-8") return data.encode("utf-8")
def run(self):
try:
while True:
data, address = self.sock.recvfrom(1024)
self.on_recv(data, address)
self.do_alive()
except KeyboardInterrupt:
self.sock.close()
def setup_ssdp(self): def setup_ssdp(self):
self.sock = None self.sock = None
@ -167,7 +173,7 @@ class SSDPServer():
if self.proto == "ipv4": if self.proto == "ipv4":
self.af_type = socket.AF_INET self.af_type = socket.AF_INET
self.broadcast_ip = "239.255.255.250" self.broadcast_ip = "239.255.255.250"
self.broadcase_address_tuple = (self.broadcast_ip, 1900) self.broadcast_address_tuple = (self.broadcast_ip, 1900)
self.bind_address = "0.0.0.0" self.bind_address = "0.0.0.0"
elif self.proto == "ipv6": elif self.proto == "ipv6":
self.af_type = socket.AF_INET6 self.af_type = socket.AF_INET6

View File

@ -1,39 +0,0 @@
class HDHR_SSDP():
def __init__(self, fhdhr, broadcast_ip, max_age):
self.fhdhr = fhdhr
self.ssdp_content = None
self.broadcast_ip = broadcast_ip
self.device_xml_path = '/device.xml'
self.max_age = max_age
def get(self):
if self.ssdp_content:
return self.ssdp_content.encode("utf-8")
data = ''
data_command = "NOTIFY * HTTP/1.1"
data_dict = {
"HOST": "%s:%s" % (self.broadcast_ip, 1900),
"NT": 'urn:schemas-upnp-org:device:MediaServer:1',
"NTS": "ssdp:alive",
"USN": 'uuid:%s::%s' % (self.fhdhr.config.dict["main"]["uuid"], 'urn:schemas-upnp-org:device:MediaServer:1'),
"SERVER": 'fHDHR/%s UPnP/1.0' % self.fhdhr.version,
"LOCATION": "%s%s" % (self.fhdhr.api.base, self.device_xml_path),
"AL": "%s%s" % (self.fhdhr.api.base, self.device_xml_path),
"Cache-Control:max-age=": self.max_age
}
data += "%s\r\n" % data_command
for data_key in list(data_dict.keys()):
data += "%s:%s\r\n" % (data_key, data_dict[data_key])
data += "\r\n"
self.ssdp_content = data
return data.encode("utf-8")

View File

@ -1,39 +0,0 @@
class RMG_SSDP():
def __init__(self, fhdhr, broadcast_ip, max_age):
self.fhdhr = fhdhr
self.ssdp_content = None
self.broadcast_ip = broadcast_ip
self.device_xml_path = '/device.xml'
self.max_age = max_age
def get(self):
if self.ssdp_content:
return self.ssdp_content.encode("utf-8")
data = ''
data_command = "NOTIFY * HTTP/1.1"
data_dict = {
"HOST": "%s:%s" % (self.broadcast_ip, 1900),
"NT": 'urn:schemas-upnp-org:device-1-0',
"NTS": "ssdp:alive",
"USN": 'uuid:%s::%s' % (self.fhdhr.config.dict["main"]["uuid"], 'urn:schemas-upnp-org:device-1-0'),
"SERVER": 'fHDHR/%s UPnP/1.0' % self.fhdhr.version,
"LOCATION": "%s%s" % (self.fhdhr.api.base, self.device_xml_path),
"AL": "%s%s" % (self.fhdhr.api.base, self.device_xml_path),
"Cache-Control:max-age=": self.max_age
}
data += "%s\r\n" % data_command
for data_key in list(data_dict.keys()):
data += "%s:%s\r\n" % (data_key, data_dict[data_key])
data += "\r\n"
self.ssdp_content = data
return data.encode("utf-8")

View File

@ -1,16 +0,0 @@
class fHDHR_Detect():
def __init__(self, fhdhr):
self.fhdhr = fhdhr
self.fhdhr.db.delete_fhdhr_value("ssdp_detect", "list")
def set(self, location):
detect_list = self.fhdhr.db.get_fhdhr_value("ssdp_detect", "list") or []
if location not in detect_list:
detect_list.append(location)
self.fhdhr.db.set_fhdhr_value("ssdp_detect", "list", detect_list)
def get(self):
return self.fhdhr.db.get_fhdhr_value("ssdp_detect", "list") or []

View File

@ -1,3 +1,4 @@
import m3u8
from fHDHR.exceptions import TunerError from fHDHR.exceptions import TunerError
@ -11,96 +12,218 @@ class Tuners():
self.channels = channels self.channels = channels
self.epg = epg self.epg = epg
self.max_tuners = int(self.fhdhr.config.dict["fhdhr"]["tuner_count"])
self.tuners = {} self.tuners = {}
for origin in list(self.fhdhr.origins.origins_dict.keys()):
self.tuners[origin] = {}
self.fhdhr.logger.info("Creating %s tuners." % str(self.max_tuners)) max_tuners = int(self.fhdhr.origins.origins_dict[origin].tuners)
for i in range(0, self.max_tuners): self.fhdhr.logger.info("Creating %s tuners for %s." % (max_tuners, origin))
self.tuners[str(i)] = Tuner(fhdhr, i, epg)
def get_available_tuner(self): for i in range(0, max_tuners):
return next(tunernum for tunernum in list(self.tuners.keys()) if not self.tuners[tunernum].tuner_lock.locked()) or None self.tuners[origin][str(i)] = Tuner(fhdhr, i, epg, origin)
def get_scanning_tuner(self): self.alt_stream_handlers = {}
return next(tunernum for tunernum in list(self.tuners.keys()) if self.tuners[tunernum].status["status"] == "Scanning") or None
def stop_tuner_scan(self): def alt_stream_methods_selfadd(self):
tunernum = self.get_scanning_tuner() for plugin_name in list(self.fhdhr.plugins.plugins.keys()):
if self.fhdhr.plugins.plugins[plugin_name].type == "alt_stream":
method = self.fhdhr.plugins.plugins[plugin_name].name
self.alt_stream_handlers[method] = self.fhdhr.plugins.plugins[plugin_name]
def get_available_tuner(self, origin):
return next(tunernum for tunernum in list(self.tuners[origin].keys()) if not self.tuners[origin][tunernum].tuner_lock.locked()) or None
def get_scanning_tuner(self, origin):
return next(tunernum for tunernum in list(self.tuners[origin].keys()) if self.tuners[origin][tunernum].status["status"] == "Scanning") or None
def stop_tuner_scan(self, origin):
tunernum = self.get_scanning_tuner(origin)
if tunernum: if tunernum:
self.tuners[str(tunernum)].close() self.tuners[origin][str(tunernum)].close()
def tuner_scan(self): def tuner_scan(self, origin="all"):
"""Temporarily use a tuner for a scan""" """Temporarily use a tuner for a scan"""
if not self.available_tuner_count():
if origin == "all":
origins = list(self.tuners.keys())
else:
origins = [origin]
for origin in origins:
if not self.available_tuner_count(origin):
raise TunerError("805 - All Tuners In Use") raise TunerError("805 - All Tuners In Use")
tunernumber = self.get_available_tuner() tunernumber = self.get_available_tuner(origin)
self.tuners[str(tunernumber)].channel_scan() self.tuners[origin][str(tunernumber)].channel_scan(origin)
if not tunernumber: if not tunernumber:
raise TunerError("805 - All Tuners In Use") raise TunerError("805 - All Tuners In Use")
def tuner_grab(self, tuner_number, channel_number): def tuner_grab(self, tuner_number, origin, channel_number):
if str(tuner_number) not in list(self.tuners.keys()): if str(tuner_number) not in list(self.tuners[origin].keys()):
self.fhdhr.logger.error("Tuner %s does not exist." % str(tuner_number)) self.fhdhr.logger.error("Tuner %s does not exist for %s." % (tuner_number, origin))
raise TunerError("806 - Tune Failed") raise TunerError("806 - Tune Failed")
# TunerError will raise if unavailable # TunerError will raise if unavailable
self.tuners[str(tuner_number)].grab(channel_number) self.tuners[origin][str(tuner_number)].grab(origin, channel_number)
return tuner_number return tuner_number
def first_available(self, channel_number): def first_available(self, origin, channel_number, dograb=True):
if not self.available_tuner_count(): if not self.available_tuner_count(origin):
raise TunerError("805 - All Tuners In Use") raise TunerError("805 - All Tuners In Use")
tunernumber = self.get_available_tuner() tunernumber = self.get_available_tuner(origin)
if not tunernumber: if not tunernumber:
raise TunerError("805 - All Tuners In Use") raise TunerError("805 - All Tuners In Use")
else: else:
self.tuners[str(tunernumber)].grab(channel_number) self.tuners[origin][str(tunernumber)].grab(origin, channel_number)
return tunernumber return tunernumber
def tuner_close(self, tunernum): def tuner_close(self, tunernum, origin):
self.tuners[str(tunernum)].close() self.tuners[origin][str(tunernum)].close()
def status(self): def status(self, origin=None):
all_status = {} all_status = {}
for tunernum in list(self.tuners.keys()): if origin:
all_status[tunernum] = self.tuners[str(tunernum)].get_status() for tunernum in list(self.tuners[origin].keys()):
all_status[tunernum] = self.tuners[origin][str(tunernum)].get_status()
else:
for origin in list(self.tuners.keys()):
all_status[origin] = {}
for tunernum in list(self.tuners[origin].keys()):
all_status[origin][tunernum] = self.tuners[origin][str(tunernum)].get_status()
return all_status return all_status
def available_tuner_count(self): def available_tuner_count(self, origin):
available_tuners = 0 available_tuners = 0
for tunernum in list(self.tuners.keys()): for tunernum in list(self.tuners[origin].keys()):
if not self.tuners[str(tunernum)].tuner_lock.locked(): if not self.tuners[origin][str(tunernum)].tuner_lock.locked():
available_tuners += 1 available_tuners += 1
return available_tuners return available_tuners
def inuse_tuner_count(self): def inuse_tuner_count(self, origin):
inuse_tuners = 0 inuse_tuners = 0
for tunernum in list(self.tuners.keys()): for tunernum in list(self.tuners[origin].keys()):
if self.tuners[str(tunernum)].tuner_lock.locked(): if self.tuners[origin][str(tunernum)].tuner_lock.locked():
inuse_tuners += 1 inuse_tuners += 1
return inuse_tuners return inuse_tuners
def get_stream_info(self, stream_args): def get_stream_info(self, stream_args):
stream_args["channelUri"] = self.channels.get_channel_stream(str(stream_args["channel"])) stream_info = self.channels.get_channel_stream(stream_args, stream_args["origin"])
if not stream_args["channelUri"]: if not stream_info:
raise TunerError("806 - Tune Failed") raise TunerError("806 - Tune Failed")
channelUri_headers = self.fhdhr.web.session.head(stream_args["channelUri"]).headers if isinstance(stream_info, str):
stream_args["true_content_type"] = channelUri_headers['Content-Type'] stream_info = {"url": stream_info, "headers": None}
stream_args["stream_info"] = stream_info
if not stream_args["stream_info"]["url"]:
raise TunerError("806 - Tune Failed")
if "headers" not in list(stream_args["stream_info"].keys()):
stream_args["stream_info"]["headers"] = None
if stream_args["stream_info"]["url"].startswith("udp://"):
stream_args["true_content_type"] = "video/mpeg"
stream_args["content_type"] = "video/mpeg"
else:
channel_stream_url_headers = self.fhdhr.web.session.head(stream_args["stream_info"]["url"]).headers
stream_args["true_content_type"] = channel_stream_url_headers['Content-Type']
if stream_args["true_content_type"].startswith(tuple(["application/", "text/"])): if stream_args["true_content_type"].startswith(tuple(["application/", "text/"])):
stream_args["content_type"] = "video/mpeg" stream_args["content_type"] = "video/mpeg"
if stream_args["origin_quality"] != -1:
stream_args["stream_info"]["url"] = self.m3u8_quality(stream_args)
else: else:
stream_args["content_type"] = stream_args["true_content_type"] stream_args["content_type"] = stream_args["true_content_type"]
return stream_args return stream_args
def m3u8_quality(self, stream_args):
m3u8_url = stream_args["stream_info"]["url"]
quality_profile = stream_args["origin_quality"]
if not quality_profile:
if stream_args["method"] == "direct":
quality_profile = "high"
self.fhdhr.logger.info("Origin Quality not set in config. Direct Method set and will default to Highest Quality")
else:
self.fhdhr.logger.info("Origin Quality not set in config. %s Method will select the Quality Automatically" % stream_args["method"])
return m3u8_url
else:
quality_profile = quality_profile.lower()
self.fhdhr.logger.info("Origin Quality set in config to %s" % (quality_profile))
while True:
self.fhdhr.logger.info("Opening m3u8 for reading %s" % m3u8_url)
try:
if stream_args["stream_info"]["headers"]:
videoUrlM3u = m3u8.load(m3u8_url, headers=stream_args["stream_info"]["headers"])
else:
videoUrlM3u = m3u8.load(m3u8_url)
except Exception as e:
self.fhdhr.logger.info("m3u8 load error: %s" % e)
return m3u8_url
if len(videoUrlM3u.playlists):
self.fhdhr.logger.info("%s m3u8 varients found" % len(videoUrlM3u.playlists))
# Create list of dicts
playlists, playlist_index = {}, 0
for playlist_item in videoUrlM3u.playlists:
playlist_index += 1
playlist_dict = {
"url": playlist_item.absolute_uri,
"bandwidth": playlist_item.stream_info.bandwidth,
}
if not playlist_item.stream_info.resolution:
playlist_dict["width"] = None
playlist_dict["height"] = None
else:
try:
playlist_dict["width"] = playlist_item.stream_info.resolution[0]
playlist_dict["height"] = playlist_item.stream_info.resolution[1]
except TypeError:
playlist_dict["width"] = None
playlist_dict["height"] = None
playlists[playlist_index] = playlist_dict
sorted_playlists = sorted(playlists, key=lambda i: (
int(playlists[i]['bandwidth']),
int(playlists[i]['width'] or 0),
int(playlists[i]['height'] or 0)
))
sorted_playlists = [playlists[x] for x in sorted_playlists]
if not quality_profile or quality_profile == "high":
selected_index = -1
elif quality_profile == "medium":
selected_index = int((len(sorted_playlists) - 1)/2)
elif quality_profile == "low":
selected_index = 0
m3u8_stats = ",".join(
["%s %s" % (x, sorted_playlists[selected_index][x])
for x in list(sorted_playlists[selected_index].keys())
if x != "url" and sorted_playlists[selected_index][x]])
self.fhdhr.logger.info("Selected m3u8 details: %s" % m3u8_stats)
m3u8_url = sorted_playlists[selected_index]["url"]
else:
self.fhdhr.logger.info("No m3u8 varients found")
break
return m3u8_url

View File

@ -2,8 +2,6 @@
from .direct_stream import Direct_Stream from .direct_stream import Direct_Stream
from .direct_m3u8_stream import Direct_M3U8_Stream from .direct_m3u8_stream import Direct_M3U8_Stream
from .ffmpeg_stream import FFMPEG_Stream
from .vlc_stream import VLC_Stream
class Stream(): class Stream():
@ -12,16 +10,14 @@ class Stream():
self.fhdhr = fhdhr self.fhdhr = fhdhr
self.stream_args = stream_args self.stream_args = stream_args
if stream_args["method"] == "ffmpeg": if stream_args["method"] == "direct":
self.method = FFMPEG_Stream(fhdhr, stream_args, tuner) if self.stream_args["true_content_type"].startswith(tuple(["application/", "text/"])):
if stream_args["method"] == "vlc":
self.method = VLC_Stream(fhdhr, stream_args, tuner)
elif (stream_args["method"] == "direct" and
not self.stream_args["true_content_type"].startswith(tuple(["application/", "text/"]))):
self.method = Direct_Stream(fhdhr, stream_args, tuner)
elif (stream_args["method"] == "direct" and
self.stream_args["true_content_type"].startswith(tuple(["application/", "text/"]))):
self.method = Direct_M3U8_Stream(fhdhr, stream_args, tuner) self.method = Direct_M3U8_Stream(fhdhr, stream_args, tuner)
else:
self.method = Direct_Stream(fhdhr, stream_args, tuner)
else:
plugin_name = self.fhdhr.config.dict["streaming"]["valid_methods"][stream_args["method"]]["plugin"]
self.method = self.fhdhr.plugins.plugins[plugin_name].Plugin_OBJ(fhdhr, self.fhdhr.plugins.plugins[plugin_name].plugin_utils, stream_args, tuner)
def get(self): def get(self):
return self.method.get() return self.method.get()

View File

@ -14,25 +14,17 @@ class Direct_M3U8_Stream():
self.stream_args = stream_args self.stream_args = stream_args
self.tuner = tuner self.tuner = tuner
self.chunksize = int(self.fhdhr.config.dict["direct_stream"]['chunksize']) self.bytes_per_read = int(self.fhdhr.config.dict["streaming"]["bytes_per_read"])
def get(self): def get(self):
if not self.stream_args["duration"] == 0: if not self.stream_args["duration"] == 0:
self.stream_args["time_end"] = self.stream_args["duration"] + time.time() self.stream_args["time_end"] = self.stream_args["duration"] + time.time()
self.fhdhr.logger.info("Detected stream URL is m3u8: %s" % self.stream_args["true_content_type"]) self.fhdhr.logger.info("Detected stream of m3u8 URL: %s" % self.stream_args["stream_info"]["url"])
channelUri = self.stream_args["channelUri"] if self.stream_args["transcode_quality"]:
while True: self.fhdhr.logger.info("Client requested a %s transcode for stream. Direct Method cannot transcode." % self.stream_args["transcode_quality"])
self.fhdhr.logger.info("Opening m3u8 for reading %s" % channelUri)
videoUrlM3u = m3u8.load(channelUri)
if len(videoUrlM3u.playlists):
self.fhdhr.logger.info("%s m3u8 varients found" % len(videoUrlM3u.playlists))
channelUri = videoUrlM3u.playlists[0].absolute_uri
else:
break
def generate(): def generate():
@ -42,7 +34,16 @@ class Direct_M3U8_Stream():
while self.tuner.tuner_lock.locked(): while self.tuner.tuner_lock.locked():
playlist = m3u8.load(channelUri) try:
if self.stream_args["stream_info"]["headers"]:
playlist = m3u8.load(self.stream_args["stream_info"]["url"], headers=self.stream_args["stream_info"]["headers"])
else:
playlist = m3u8.load(self.stream_args["stream_info"]["url"])
except Exception as e:
self.fhdhr.logger.info("Connection Closed: %s" % e)
self.tuner.close()
return None
segments = playlist.segments segments = playlist.segments
if len(played_chunk_urls): if len(played_chunk_urls):
@ -70,12 +71,18 @@ class Direct_M3U8_Stream():
self.fhdhr.logger.info("Requested Duration Expired.") self.fhdhr.logger.info("Requested Duration Expired.")
self.tuner.close() self.tuner.close()
if self.stream_args["stream_info"]["headers"]:
chunk = self.fhdhr.web.session.get(chunkurl, headers=self.stream_args["stream_info"]["headers"]).content
else:
chunk = self.fhdhr.web.session.get(chunkurl).content chunk = self.fhdhr.web.session.get(chunkurl).content
if not chunk: if not chunk:
break break
# raise TunerError("807 - No Video Data") # raise TunerError("807 - No Video Data")
if key: if key:
if key["url"]: if key["url"]:
if self.stream_args["stream_info"]["headers"]:
keyfile = self.fhdhr.web.session.get(key["url"], headers=self.stream_args["stream_info"]["headers"]).content
else:
keyfile = self.fhdhr.web.session.get(key["url"]).content keyfile = self.fhdhr.web.session.get(key["url"]).content
cryptor = AES.new(keyfile, AES.MODE_CBC, keyfile) cryptor = AES.new(keyfile, AES.MODE_CBC, keyfile)
self.fhdhr.logger.info("Decrypting Chunk #%s with key: %s" % (len(played_chunk_urls), key["url"])) self.fhdhr.logger.info("Decrypting Chunk #%s with key: %s" % (len(played_chunk_urls), key["url"]))
@ -91,8 +98,11 @@ class Direct_M3U8_Stream():
except GeneratorExit: except GeneratorExit:
self.fhdhr.logger.info("Connection Closed.") self.fhdhr.logger.info("Connection Closed.")
except Exception as e: except Exception as e:
self.fhdhr.logger.info("Connection Closed: " + str(e)) self.fhdhr.logger.info("Connection Closed: %s" % e)
finally: finally:
self.fhdhr.logger.info("Connection Closed: Tuner Lock Removed")
if hasattr(self.fhdhr.origins.origins_dict[self.tuner.origin], "close_stream"):
self.fhdhr.origins.origins_dict[self.tuner.origin].close_stream(self.tuner.number, self.stream_args)
self.tuner.close() self.tuner.close()
# raise TunerError("806 - Tune Failed") # raise TunerError("806 - Tune Failed")

View File

@ -11,16 +11,22 @@ class Direct_Stream():
self.stream_args = stream_args self.stream_args = stream_args
self.tuner = tuner self.tuner = tuner
self.chunksize = int(self.fhdhr.config.dict["direct_stream"]['chunksize']) self.bytes_per_read = int(self.fhdhr.config.dict["streaming"]["bytes_per_read"])
def get(self): def get(self):
if not self.stream_args["duration"] == 0: if not self.stream_args["duration"] == 0:
self.stream_args["time_end"] = self.stream_args["duration"] + time.time() self.stream_args["time_end"] = self.stream_args["duration"] + time.time()
self.fhdhr.logger.info("Direct Stream of %s URL: %s" % (self.stream_args["true_content_type"], self.stream_args["channelUri"])) self.fhdhr.logger.info("Direct Stream of %s URL: %s" % (self.stream_args["true_content_type"], self.stream_args["stream_info"]["url"]))
req = self.fhdhr.web.session.get(self.stream_args["channelUri"], stream=True) if self.stream_args["transcode_quality"]:
self.fhdhr.logger.info("Client requested a %s transcode for stream. Direct Method cannot transcode." % self.stream_args["transcode_quality"])
if self.stream_args["stream_info"]["headers"]:
req = self.fhdhr.web.session.get(self.stream_args["stream_info"]["url"], stream=True, headers=self.stream_args["stream_info"]["headers"])
else:
req = self.fhdhr.web.session.get(self.stream_args["stream_info"]["url"], stream=True)
def generate(): def generate():
@ -30,7 +36,7 @@ class Direct_Stream():
while self.tuner.tuner_lock.locked(): while self.tuner.tuner_lock.locked():
for chunk in req.iter_content(chunk_size=self.chunksize): for chunk in req.iter_content(chunk_size=self.bytes_per_read):
if (not self.stream_args["duration"] == 0 and if (not self.stream_args["duration"] == 0 and
not time.time() < self.stream_args["time_end"]): not time.time() < self.stream_args["time_end"]):
@ -54,9 +60,12 @@ class Direct_Stream():
except GeneratorExit: except GeneratorExit:
self.fhdhr.logger.info("Connection Closed.") self.fhdhr.logger.info("Connection Closed.")
except Exception as e: except Exception as e:
self.fhdhr.logger.info("Connection Closed: " + str(e)) self.fhdhr.logger.info("Connection Closed: %s" % e)
finally: finally:
req.close() req.close()
self.fhdhr.logger.info("Connection Closed: Tuner Lock Removed")
if hasattr(self.fhdhr.origins.origins_dict[self.tuner.origin], "close_stream"):
self.fhdhr.origins.origins_dict[self.tuner.origin].close_stream(self.tuner.number, self.stream_args)
self.tuner.close() self.tuner.close()
# raise TunerError("806 - Tune Failed") # raise TunerError("806 - Tune Failed")

View File

@ -1,130 +0,0 @@
import sys
import subprocess
# from fHDHR.exceptions import TunerError
class FFMPEG_Stream():
def __init__(self, fhdhr, stream_args, tuner):
self.fhdhr = fhdhr
self.stream_args = stream_args
self.tuner = tuner
self.bytes_per_read = int(self.fhdhr.config.dict["ffmpeg"]["bytes_per_read"])
self.ffmpeg_command = self.ffmpeg_command_assemble(stream_args)
def get(self):
ffmpeg_proc = subprocess.Popen(self.ffmpeg_command, stdout=subprocess.PIPE)
def generate():
try:
while self.tuner.tuner_lock.locked():
chunk = ffmpeg_proc.stdout.read(self.bytes_per_read)
if not chunk:
break
# raise TunerError("807 - No Video Data")
yield chunk
chunk_size = int(sys.getsizeof(chunk))
self.tuner.add_downloaded_size(chunk_size)
self.fhdhr.logger.info("Connection Closed: Tuner Lock Removed")
except GeneratorExit:
self.fhdhr.logger.info("Connection Closed.")
except Exception as e:
self.fhdhr.logger.info("Connection Closed: " + str(e))
finally:
ffmpeg_proc.terminate()
ffmpeg_proc.communicate()
self.tuner.close()
# raise TunerError("806 - Tune Failed")
return generate()
def ffmpeg_command_assemble(self, stream_args):
ffmpeg_command = [
self.fhdhr.config.dict["ffmpeg"]["path"],
"-i", stream_args["channelUri"],
]
ffmpeg_command.extend(self.ffmpeg_duration(stream_args))
ffmpeg_command.extend(self.transcode_profiles(stream_args))
ffmpeg_command.extend(self.ffmpeg_loglevel())
ffmpeg_command.extend(["pipe:stdout"])
return ffmpeg_command
def ffmpeg_duration(self, stream_args):
ffmpeg_command = []
if stream_args["duration"]:
ffmpeg_command.extend(["-t", str(stream_args["duration"])])
else:
ffmpeg_command.extend(
[
"-reconnect", "1",
"-reconnect_at_eof", "1",
"-reconnect_streamed", "1",
"-reconnect_delay_max", "2",
]
)
return ffmpeg_command
def ffmpeg_loglevel(self):
ffmpeg_command = []
log_level = self.fhdhr.config.dict["logging"]["level"].lower()
loglevel_dict = {
"debug": "debug",
"info": "info",
"error": "error",
"warning": "warning",
"critical": "fatal",
}
if log_level not in ["info", "debug"]:
ffmpeg_command.extend(["-nostats", "-hide_banner"])
ffmpeg_command.extend(["-loglevel", loglevel_dict[log_level]])
return ffmpeg_command
def transcode_profiles(self, stream_args):
# TODO implement actual profiles here
"""
heavy: transcode to AVC with the same resolution, frame-rate, and interlacing as the
original stream. For example 1080i60 AVC 1080i60, 720p60 AVC 720p60.
mobile: trancode to AVC progressive not exceeding 1280x720 30fps.
internet720: transcode to low bitrate AVC progressive not exceeding 1280x720 30fps.
internet480: transcode to low bitrate AVC progressive not exceeding 848x480 30fps for
16:9 content, not exceeding 640x480 30fps for 4:3 content.
internet360: transcode to low bitrate AVC progressive not exceeding 640x360 30fps for
16:9 content, not exceeding 480x360 30fps for 4:3 content.
internet240: transcode to low bitrate AVC progressive not exceeding 432x240 30fps for
16:9 content, not exceeding 320x240 30fps for 4:3 content
"""
if stream_args["transcode"]:
self.fhdhr.logger.info("Client requested a " + stream_args["transcode"] + " transcode for stream.")
stream_args["transcode"] = None
ffmpeg_command = []
if not stream_args["transcode"]:
ffmpeg_command.extend(
[
"-c", "copy",
"-f", "mpegts",
]
)
elif stream_args["transcode"] == "heavy":
ffmpeg_command.extend([])
elif stream_args["transcode"] == "mobile":
ffmpeg_command.extend([])
elif stream_args["transcode"] == "internet720":
ffmpeg_command.extend([])
elif stream_args["transcode"] == "internet480":
ffmpeg_command.extend([])
elif stream_args["transcode"] == "internet360":
ffmpeg_command.extend([])
elif stream_args["transcode"] == "internet240":
ffmpeg_command.extend([])
return ffmpeg_command

View File

@ -1,121 +0,0 @@
import sys
import subprocess
# from fHDHR.exceptions import TunerError
class VLC_Stream():
def __init__(self, fhdhr, stream_args, tuner):
self.fhdhr = fhdhr
self.stream_args = stream_args
self.tuner = tuner
self.bytes_per_read = int(self.fhdhr.config.dict["vlc"]["bytes_per_read"])
self.vlc_command = self.vlc_command_assemble(stream_args)
def get(self):
vlc_proc = subprocess.Popen(self.vlc_command, stdout=subprocess.PIPE)
def generate():
try:
while self.tuner.tuner_lock.locked():
chunk = vlc_proc.stdout.read(self.bytes_per_read)
if not chunk:
break
# raise TunerError("807 - No Video Data")
yield chunk
chunk_size = int(sys.getsizeof(chunk))
self.tuner.add_downloaded_size(chunk_size)
self.fhdhr.logger.info("Connection Closed: Tuner Lock Removed")
except GeneratorExit:
self.fhdhr.logger.info("Connection Closed.")
except Exception as e:
self.fhdhr.logger.info("Connection Closed: " + str(e))
finally:
vlc_proc.terminate()
vlc_proc.communicate()
self.fhdhr.logger.info("Connection Closed: Tuner Lock Removed")
self.tuner.close()
# raise TunerError("806 - Tune Failed")
return generate()
def vlc_command_assemble(self, stream_args):
vlc_command = [
self.fhdhr.config.dict["vlc"]["path"],
"-I", "dummy", stream_args["channelUri"],
]
vlc_command.extend(self.vlc_duration(stream_args))
vlc_command.extend(self.vlc_loglevel())
vlc_command.extend(["--sout"])
vlc_command.extend(self.transcode_profiles(stream_args))
return vlc_command
def vlc_duration(self, stream_args):
vlc_command = []
if stream_args["duration"]:
vlc_command.extend(["--run-time=%s" % str(stream_args["duration"])])
return vlc_command
def vlc_loglevel(self):
vlc_command = []
log_level = self.fhdhr.config.dict["logging"]["level"].lower()
loglevel_dict = {
"debug": "3",
"info": "0",
"error": "1",
"warning": "2",
"critical": "1",
}
vlc_command.extend(["--log-verbose=", loglevel_dict[log_level]])
if log_level not in ["info", "debug"]:
vlc_command.extend(["--quiet"])
return vlc_command
def transcode_profiles(self, stream_args):
# TODO implement actual profiles here
"""
heavy: transcode to AVC with the same resolution, frame-rate, and interlacing as the
original stream. For example 1080i60 AVC 1080i60, 720p60 AVC 720p60.
mobile: trancode to AVC progressive not exceeding 1280x720 30fps.
internet720: transcode to low bitrate AVC progressive not exceeding 1280x720 30fps.
internet480: transcode to low bitrate AVC progressive not exceeding 848x480 30fps for
16:9 content, not exceeding 640x480 30fps for 4:3 content.
internet360: transcode to low bitrate AVC progressive not exceeding 640x360 30fps for
16:9 content, not exceeding 480x360 30fps for 4:3 content.
internet240: transcode to low bitrate AVC progressive not exceeding 432x240 30fps for
16:9 content, not exceeding 320x240 30fps for 4:3 content
"""
vlc_command = []
if stream_args["transcode"]:
self.fhdhr.logger.info("Client requested a " + stream_args["transcode"] + " transcode for stream.")
stream_args["transcode"] = None
vlc_transcode_string = "#std{mux=ts,access=file,dst=-}"
return [vlc_transcode_string]
'#transcode{vcodec=mp2v,vb=4096,acodec=mp2a,ab=192,scale=1,channels=2,deinterlace}:std{access=file,mux=ts,dst=-"}'
if not stream_args["transcode"]:
vlc_command.extend([])
elif stream_args["transcode"] == "heavy":
vlc_command.extend([])
elif stream_args["transcode"] == "mobile":
vlc_command.extend([])
elif stream_args["transcode"] == "internet720":
vlc_command.extend([])
elif stream_args["transcode"] == "internet480":
vlc_command.extend([])
elif stream_args["transcode"] == "internet360":
vlc_command.extend([])
elif stream_args["transcode"] == "internet240":
vlc_command.extend([])
return vlc_command

View File

@ -1,4 +1,3 @@
import multiprocessing
import threading import threading
import datetime import datetime
@ -9,70 +8,76 @@ from .stream import Stream
class Tuner(): class Tuner():
def __init__(self, fhdhr, inum, epg): def __init__(self, fhdhr, inum, epg, origin):
self.fhdhr = fhdhr self.fhdhr = fhdhr
self.number = inum self.number = inum
self.origin = origin
self.epg = epg self.epg = epg
self.tuner_lock = threading.Lock() self.tuner_lock = threading.Lock()
self.set_off_status() self.set_off_status()
self.chanscan_url = "%s/api/channels?method=scan" % (self.fhdhr.api.base) self.chanscan_url = "/api/channels?method=scan"
self.close_url = "%s/api/tuners?method=close&tuner=%s" % (self.fhdhr.api.base, str(self.number)) self.close_url = "/api/tuners?method=close&tuner=%s&origin=%s" % (self.number, self.origin)
def channel_scan(self): def channel_scan(self, origin, grabbed=False):
if self.tuner_lock.locked(): if self.tuner_lock.locked() and not grabbed:
self.fhdhr.logger.error("Tuner #%s is not available." % str(self.number)) self.fhdhr.logger.error("%s Tuner #%s is not available." % (self.origin, self.number))
raise TunerError("804 - Tuner In Use") raise TunerError("804 - Tuner In Use")
if self.status["status"] == "Scanning": if self.status["status"] == "Scanning":
self.fhdhr.logger.info("Channel Scan Already In Progress!") self.fhdhr.logger.info("Channel Scan Already In Progress!")
else: else:
if not grabbed:
self.tuner_lock.acquire() self.tuner_lock.acquire()
self.status["status"] = "Scanning" self.status["status"] = "Scanning"
self.fhdhr.logger.info("Tuner #%s Performing Channel Scan." % str(self.number)) self.status["origin"] = origin
self.status["time_start"] = datetime.datetime.utcnow()
self.fhdhr.logger.info("Tuner #%s Performing Channel Scan for %s origin." % (self.number, origin))
if self.fhdhr.config.dict["main"]["thread_method"] in ["multiprocessing"]: chanscan = threading.Thread(target=self.runscan, args=(origin,))
chanscan = multiprocessing.Process(target=self.runscan)
elif self.fhdhr.config.dict["main"]["thread_method"] in ["threading"]:
chanscan = threading.Thread(target=self.runscan)
if self.fhdhr.config.dict["main"]["thread_method"] in ["multiprocessing", "threading"]:
chanscan.start() chanscan.start()
def runscan(self): def runscan(self, origin):
self.fhdhr.web.session.get(self.chanscan_url) self.fhdhr.api.get("%s&origin=%s" % (self.chanscan_url, origin))
self.fhdhr.logger.info("Requested Channel Scan Complete.") self.fhdhr.logger.info("Requested Channel Scan for %s origin Complete." % origin)
self.fhdhr.web.session.get(self.close_url) self.close()
self.fhdhr.api.get(self.close_url)
def add_downloaded_size(self, bytes_count): def add_downloaded_size(self, bytes_count):
if "downloaded" in list(self.status.keys()): if "downloaded" in list(self.status.keys()):
self.status["downloaded"] += bytes_count self.status["downloaded"] += bytes_count
def grab(self, channel_number): def grab(self, origin, channel_number):
if self.tuner_lock.locked(): if self.tuner_lock.locked():
self.fhdhr.logger.error("Tuner #" + str(self.number) + " is not available.") self.fhdhr.logger.error("Tuner #%s is not available." % self.number)
raise TunerError("804 - Tuner In Use") raise TunerError("804 - Tuner In Use")
self.tuner_lock.acquire() self.tuner_lock.acquire()
self.status["status"] = "Acquired" self.status["status"] = "Acquired"
self.status["origin"] = origin
self.status["channel"] = channel_number self.status["channel"] = channel_number
self.status["time_start"] = datetime.datetime.utcnow()
self.fhdhr.logger.info("Tuner #%s Acquired." % str(self.number)) self.fhdhr.logger.info("Tuner #%s Acquired." % str(self.number))
def close(self): def close(self):
self.set_off_status() self.set_off_status()
if self.tuner_lock.locked(): if self.tuner_lock.locked():
self.tuner_lock.release() self.tuner_lock.release()
self.fhdhr.logger.info("Tuner #" + str(self.number) + " Released.") self.fhdhr.logger.info("Tuner #%s Released." % self.number)
def get_status(self): def get_status(self):
current_status = self.status.copy() current_status = self.status.copy()
if current_status["status"] == "Active": current_status["epg"] = {}
current_status["Play Time"] = str( if current_status["status"] in ["Acquired", "Active", "Scanning"]:
current_status["running_time"] = str(
humanized_time( humanized_time(
int((datetime.datetime.utcnow() - current_status["time_start"]).total_seconds()))) int((datetime.datetime.utcnow() - current_status["time_start"]).total_seconds())))
current_status["time_start"] = str(current_status["time_start"]) current_status["time_start"] = str(current_status["time_start"])
current_status["epg"] = self.epg.whats_on_now(current_status["channel"]) if current_status["status"] in ["Active"]:
if current_status["origin"] in self.epg.epg_methods:
current_status["epg"] = self.epg.whats_on_now(current_status["channel"], method=current_status["origin"])
return current_status return current_status
def set_off_status(self): def set_off_status(self):
@ -80,7 +85,7 @@ class Tuner():
def get_stream(self, stream_args, tuner): def get_stream(self, stream_args, tuner):
stream = Stream(self.fhdhr, stream_args, tuner) stream = Stream(self.fhdhr, stream_args, tuner)
return stream.get() return stream
def set_status(self, stream_args): def set_status(self, stream_args):
if self.status["status"] != "Active": if self.status["status"] != "Active":
@ -90,8 +95,9 @@ class Tuner():
"clients_id": [], "clients_id": [],
"method": stream_args["method"], "method": stream_args["method"],
"accessed": [stream_args["accessed"]], "accessed": [stream_args["accessed"]],
"origin": stream_args["origin"],
"channel": stream_args["channel"], "channel": stream_args["channel"],
"proxied_url": stream_args["channelUri"], "proxied_url": stream_args["stream_info"]["url"],
"time_start": datetime.datetime.utcnow(), "time_start": datetime.datetime.utcnow(),
"downloaded": 0 "downloaded": 0
} }

36
fHDHR/logger/__init__.py Normal file
View File

@ -0,0 +1,36 @@
import os
import logging
class Logger():
def __init__(self, settings):
self.config = settings
log_level = self.config.dict["logging"]["level"].upper()
# Create a custom logger
logging.basicConfig(format='%(name)s - %(levelname)s - %(message)s', level=log_level)
self.logger = logging.getLogger('fHDHR')
log_file = os.path.join(self.config.internal["paths"]["logs_dir"], 'fHDHR.log')
# Create handlers
# c_handler = logging.StreamHandler()
f_handler = logging.FileHandler(log_file)
# c_handler.setLevel(log_level)
f_handler.setLevel(log_level)
# Create formatters and add it to handlers
# c_format = logging.Formatter('%(name)s - %(levelname)s - %(message)s')
f_format = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
# c_handler.setFormatter(c_format)
f_handler.setFormatter(f_format)
# Add handlers to the logger
# logger.addHandler(c_handler)
self.logger.addHandler(f_handler)
def __getattr__(self, name):
''' will only get called for undefined attributes '''
if hasattr(self.logger, name):
return eval("self.logger.%s" % name)

48
fHDHR/origins/__init__.py Normal file
View File

@ -0,0 +1,48 @@
import fHDHR.exceptions
class Origin_StandIN():
def __init__(self):
self.setup_success = False
def get_channels(self):
return []
def get_channel_stream(self, chandict, stream_args):
return None
class Origins():
def __init__(self, fhdhr):
self.fhdhr = fhdhr
self.origins_dict = {}
self.origin_selfadd()
for plugin_name in list(self.fhdhr.plugins.plugins.keys()):
if self.fhdhr.plugins.plugins[plugin_name].manifest["tagged_mod"] and self.fhdhr.plugins.plugins[plugin_name].manifest["tagged_mod_type"] == "origin":
self.fhdhr.plugins.plugins[plugin_name].plugin_utils.origin = self.origins_dict[self.fhdhr.plugins.plugins[plugin_name].manifest["tagged_mod"].lower()]
@property
def valid_origins(self):
return [origin for origin in list(self.origins_dict.keys())]
def origin_selfadd(self):
for plugin_name in list(self.fhdhr.plugins.plugins.keys()):
if self.fhdhr.plugins.plugins[plugin_name].type == "origin":
method = self.fhdhr.plugins.plugins[plugin_name].name.lower()
try:
plugin_utils = self.fhdhr.plugins.plugins[plugin_name].plugin_utils
self.origins_dict[method] = self.fhdhr.plugins.plugins[plugin_name].Plugin_OBJ(plugin_utils)
self.fhdhr.logger.info("%s Setup Success" % method)
self.origins_dict[method].setup_success = True
except fHDHR.exceptions.OriginSetupError as e:
self.fhdhr.logger.error(e)
self.origins_dict[method] = Origin_StandIN()
if not hasattr(self.origins_dict[method], 'tuners'):
self.origins_dict[method].tuners = 4
if not hasattr(self.origins_dict[method], 'stream_method'):
self.origins_dict[method].stream_method = self.fhdhr.config.dict["streaming"]["method"]

View File

@ -1,92 +0,0 @@
import fHDHR.exceptions
class OriginEPG_StandIN():
def __init__(self):
pass
def update_epg(self, channels):
return {}
class OriginChannels_StandIN():
def __init__(self):
pass
def get_channels(self):
return []
def get_channel_stream(self, chandict):
return None
class OriginServiceWrapper():
def __init__(self, fhdhr, origin):
self.fhdhr = fhdhr
self.origin = origin
self.servicename = fhdhr.config.dict["main"]["servicename"]
self.setup_success = None
self.setup()
def setup(self):
try:
self.originservice = self.origin.OriginService(self.fhdhr)
self.setup_success = True
self.fhdhr.logger.info("%s Setup Success" % self.servicename)
except fHDHR.exceptions.OriginSetupError as e:
self.fhdhr.logger.error(e)
self.setup_success = False
if self.setup_success:
self.channels = self.origin.OriginChannels(self.fhdhr, self.originservice)
self.epg = self.origin.OriginEPG(self.fhdhr)
else:
self.channels = OriginChannels_StandIN()
self.epg = OriginEPG_StandIN()
def get_channels(self):
return self.channels.get_channels()
def get_channel_stream(self, chandict):
return self.channels.get_channel_stream(chandict)
def update_epg(self, channels):
return self.epg.update_epg(channels)
def get_status_dict(self):
if self.setup_success:
status_dict = {
"Setup": "Success",
}
try:
full_status_dict = self.origin.get_status_dict()
for status_key in list(full_status_dict.keys()):
status_dict[status_key] = full_status_dict[status_key]
return status_dict
except AttributeError:
return status_dict
else:
return {
"Setup": "Failed",
}
def __getattr__(self, name):
''' will only get called for undefined attributes '''
if hasattr(self.fhdhr, name):
return eval("self.fhdhr." + name)
if hasattr(self.originservice, name):
return eval("self.originservice." + name)
elif hasattr(self.channels, name):
return eval("self.channels." + name)
elif hasattr(self.epg, name):
return eval("self.epg." + name)
else:
raise AttributeError(name)

250
fHDHR/plugins/__init__.py Normal file
View File

@ -0,0 +1,250 @@
import os
import imp
import json
class Plugin_DB():
def __init__(self, db, name):
self._db = db
self.name = name
self.namespace = name.lower()
# fhdhr
def set_fhdhr_value(self, pluginitem, key, value, namespace="default"):
print("%s plugin is not allowed write access to fhdhr db namespaces." % self.name)
return
def get_fhdhr_value(self, pluginitem, key, namespace="default"):
return self._db.get_fhdhr_value(pluginitem, key, namespace=namespace.lower())
def delete_fhdhr_value(self, pluginitem, key, namespace="default"):
print("%s plugin is not allowed write access to fhdhr db namespaces." % self.name)
return
# Plugin
def set_plugin_value(self, pluginitem, key, value, namespace=None):
if not namespace:
namespace = self.namespace
elif namespace.lower() != self.namespace:
print("%s plugin is not allowed write access to %s db namespace." % (self.name, namespace))
return
return self._db.set_plugin_value(pluginitem, key, value, namespace=self.namespace)
def get_plugin_value(self, pluginitem, key, namespace=None):
if not namespace:
namespace = self.namespace
return self._db.get_plugin_value(pluginitem, key, namespace=namespace.lower())
def delete_plugin_value(self, pluginitem, key, namespace=None):
if not namespace:
namespace = self.namespace
elif namespace.lower() != self.namespace:
print("%s plugin is not allowed write access to %s db namespace." % (self.name, namespace))
return
return self._db.delete_plugin_value(pluginitem, key, namespace=self.namespace)
class Plugin_Config():
def __init__(self, config, name):
self._config = config
self.name = name
self.namespace = name.lower()
@property
def dict(self):
return self._config.dict.copy()
@property
def internal(self):
return self._config.internal.copy()
@property
def conf_default(self):
return self._config.conf_default.copy()
def write(self, key, value, namespace=None):
if not namespace:
namespace = self.namespace
elif str(namespace).lower() != self.namespace:
print("%s plugin is not allowed write access to fhdhr config namespaces." % self.name)
return
return self._config.write(key, value, self.namespace)
class Plugin_Utils():
def __init__(self, config, logger, db, plugin_name, plugin_manifest, modname):
self.config = Plugin_Config(config, plugin_manifest["name"])
self.db = Plugin_DB(db, plugin_manifest["name"])
self.logger = logger
self.namespace = plugin_manifest["name"].lower()
self.plugin_name = plugin_name
self.plugin_manifest = plugin_manifest
self.origin = None
class Plugin():
def __init__(self, config, logger, db, plugin_name, plugin_path, plugin_conf, plugin_manifest):
self.config = config
self.db = db
self.logger = logger
# Gather Info about Plugin
self.plugin_name = plugin_name
self.modname = os.path.basename(plugin_path)
self.path = plugin_path
self.module_type = imp.PKG_DIRECTORY
self.multi_plugin = (self.plugin_name != self.modname)
self.default_conf = plugin_conf
self.manifest = plugin_manifest
if self.multi_plugin:
self.plugin_dict_name = "%s_%s" % (plugin_name, self.modname)
else:
self.plugin_dict_name = plugin_name
self.plugin_utils = Plugin_Utils(config, logger, db, plugin_name, plugin_manifest, self.modname)
# Load the module
self._module = self._load()
def setup(self):
if self.type == "alt_epg":
self.config.register_valid_epg_method(self.name, self.plugin_dict_name)
elif self.type == "alt_stream":
self.config.register_valid_streaming_method(self.name, self.plugin_dict_name)
elif self.type == "web":
self.config.register_web_path(self.manifest["name"], self.path, self.plugin_dict_name)
if self.has_setup():
self._module.setup(self)
def has_setup(self):
return hasattr(self._module, 'setup')
def _load(self):
description = ('', '', self.module_type)
mod = imp.load_module(self.plugin_dict_name, None, self.path, description)
return mod
@property
def name(self):
return self.manifest["name"]
@property
def version(self):
return self.manifest["version"]
@property
def type(self):
return self.manifest["type"]
def __getattr__(self, name):
''' will only get called for undefined attributes '''
if name == "Plugin_OBJ":
return self._module.Plugin_OBJ
class PluginsHandler():
def __init__(self, settings):
self.config = settings
self.plugins = {}
self.found_plugins = []
self.found_plugins_conf = []
self.list_plugins()
def setup(self):
for plugin_name in list(self.plugins.keys()):
self.plugins[plugin_name].setup()
def load_plugin_configs(self):
for file_item_path in self.found_plugins_conf:
self.config.import_conf_json(file_item_path)
def list_plugins(self):
for directory in self.config.internal["paths"]["plugins_dir"]:
base = os.path.abspath(directory)
for filename in os.listdir(base):
abspath = os.path.join(base, filename)
if os.path.isdir(abspath):
plugin_conf = []
for subfilename in os.listdir(abspath):
subabspath = os.path.join(abspath, subfilename)
if subfilename.endswith("_conf.json"):
plugin_conf.append(subabspath)
self.found_plugins_conf.append(subabspath)
# Plugin/multi-plugin must have a basic manifest json
conffilepath = os.path.join(abspath, 'plugin.json')
if os.path.isfile(conffilepath):
plugin_manifest = json.load(open(conffilepath, 'r'))
for plugin_man_item in ["name", "version", "type"]:
if plugin_man_item not in list(plugin_manifest.keys()):
plugin_manifest[plugin_man_item] = None
self.config.register_version(os.path.basename(filename), plugin_manifest["version"], "plugin")
if plugin_manifest["type"] == "origin":
self.config.register_valid_origin_method(plugin_manifest["name"])
plugin_import_print_string = "Found %s type plugin: %s %s. " % (plugin_manifest["type"], plugin_manifest["name"], plugin_manifest["version"])
# Warn for multiple origins
if plugin_manifest["type"] == "origin" and len([plugin_name for plugin_name, plugin_path, plugin_conf, plugin_manifest in self.found_plugins if plugin_manifest["type"] == "origin"]):
plugin_import_print_string += " ImportWarning: Only one Origin Allowed."
if not any(plugin_manifest[plugin_item] for plugin_item in ["name", "version", "type"]):
plugin_import_print_string += " ImportWarning: Missing PLUGIN_* Value."
else:
# Single Plugin
if os.path.isfile(os.path.join(abspath, '__init__.py')):
plugin_manifest["tagged_mod"] = None
plugin_manifest["tagged_mod_type"] = None
self.found_plugins.append((os.path.basename(filename), abspath, plugin_conf, plugin_manifest))
else:
# Multi-Plugin
for subfilename in os.listdir(abspath):
subabspath = os.path.join(abspath, subfilename)
if os.path.isdir(subabspath):
subconffilepath = os.path.join(subabspath, 'plugin.json')
if os.path.isfile(subconffilepath):
subplugin_manifest = json.load(open(subconffilepath, 'r'))
for subplugin_man_item in ["name", "version", "type"]:
if subplugin_man_item not in list(subplugin_manifest.keys()):
subplugin_manifest[subplugin_man_item] = plugin_manifest[subplugin_man_item]
else:
subplugin_manifest = plugin_manifest
subplugin_manifest["tagged_mod"] = None
subplugin_manifest["tagged_mod_type"] = None
if plugin_manifest["type"] != subplugin_manifest["type"]:
subplugin_manifest["tagged_mod"] = plugin_manifest["name"]
subplugin_manifest["tagged_mod_type"] = plugin_manifest["type"]
if os.path.isfile(os.path.join(subabspath, '__init__.py')):
self.found_plugins.append((os.path.basename(filename), subabspath, plugin_conf, subplugin_manifest))
print(plugin_import_print_string)
self.load_plugin_configs()
def load_plugins(self, logger, db):
self.logger = logger
self.db = db
for plugin_name, plugin_path, plugin_conf, plugin_manifest in self.found_plugins:
plugin_item = Plugin(self.config, self.logger, self.db, plugin_name, plugin_path, plugin_conf, plugin_manifest)
self.plugins[plugin_item.plugin_dict_name] = plugin_item

View File

@ -8,6 +8,19 @@ UNARY_OPS = (ast.UAdd, ast.USub)
BINARY_OPS = (ast.Add, ast.Sub, ast.Mult, ast.Div, ast.Mod) BINARY_OPS = (ast.Add, ast.Sub, ast.Mult, ast.Div, ast.Mod)
def channel_sort(channel_list):
"""Take a list of channel number strings and sort the Numbers and SubNumbers"""
chan_dict_list_split = {}
for number in channel_list:
try:
subnumber = number.split(".")[1]
except IndexError:
subnumber = None
prinumber = number.split(".")[0]
chan_dict_list_split[number] = {"number": prinumber, "subnumber": subnumber}
return sorted(chan_dict_list_split, key=lambda i: (int(chan_dict_list_split[i]['number']), int(chan_dict_list_split[i]['subnumber'] or 0)))
def is_docker(): def is_docker():
path = "/proc/self/cgroup" path = "/proc/self/cgroup"
if not os.path.isfile(path): if not os.path.isfile(path):
@ -120,9 +133,9 @@ def humanized_time(countdownseconds):
if currenttimevar > 1: if currenttimevar > 1:
timetype = str(x+"s") timetype = str(x+"s")
if displaymsg: if displaymsg:
displaymsg = str(displaymsg + " " + str(int(currenttimevar)) + " " + timetype) displaymsg = "%s %s %s" % (displaymsg, int(currenttimevar), timetype)
else: else:
displaymsg = str(str(int(currenttimevar)) + " " + timetype) displaymsg = "%s %s" % (int(currenttimevar), timetype)
if not displaymsg: if not displaymsg:
return "just now" return "just now"
return displaymsg return displaymsg
@ -134,3 +147,8 @@ class WebReq():
def __init__(self): def __init__(self):
self.session = requests.Session() self.session = requests.Session()
self.exceptions = requests.exceptions self.exceptions = requests.exceptions
def __getattr__(self, name):
''' will only get called for undefined attributes '''
if hasattr(self.session, name):
return eval("self.session.%s" % name)

View File

@ -1,14 +1,15 @@
from gevent.pywsgi import WSGIServer from gevent.pywsgi import WSGIServer
from flask import Flask, request from flask import Flask, request, session
import threading
import uuid
from .pages import fHDHR_Pages from .pages import fHDHR_Pages
from .files import fHDHR_Files from .files import fHDHR_Files
from .hdhr import fHDHR_HDHR from .brython import fHDHR_Brython
from .rmg import fHDHR_RMG
from .api import fHDHR_API from .api import fHDHR_API
fHDHR_web_VERSION = "v0.4.0-beta" fHDHR_web_VERSION = "v0.8.1-beta"
class fHDHR_HTTP_Server(): class fHDHR_HTTP_Server():
@ -21,55 +22,182 @@ class fHDHR_HTTP_Server():
self.fhdhr.logger.info("Loading Flask.") self.fhdhr.logger.info("Loading Flask.")
self.app = Flask("fHDHR", template_folder=self.template_folder) self.fhdhr.app = Flask("fHDHR", template_folder=self.template_folder)
self.instance_id = str(uuid.uuid4())
self.fhdhr.logger.info("Loading HTTP Pages Endpoints.") # Allow Internal API Usage
self.pages = fHDHR_Pages(fhdhr) self.fhdhr.app.testing = True
self.add_endpoints(self.pages, "pages") self.fhdhr.api.client = self.fhdhr.app.test_client()
self.fhdhr.logger.info("Loading HTTP Files Endpoints.") # Set Secret Key For Sessions
self.files = fHDHR_Files(fhdhr) self.fhdhr.app.secret_key = self.fhdhr.config.dict["fhdhr"]["friendlyname"]
self.add_endpoints(self.files, "files")
self.fhdhr.logger.info("Loading HTTP HDHR Endpoints.") self.route_list = {}
self.hdhr = fHDHR_HDHR(fhdhr)
self.add_endpoints(self.hdhr, "hdhr")
self.fhdhr.logger.info("Loading HTTP RMG Endpoints.") self.endpoints_obj = {}
self.rmg = fHDHR_RMG(fhdhr) self.endpoints_obj["pages"] = fHDHR_Pages(fhdhr)
self.add_endpoints(self.rmg, "rmg") self.endpoints_obj["files"] = fHDHR_Files(fhdhr)
self.endpoints_obj["brython"] = fHDHR_Brython(fhdhr)
self.endpoints_obj["api"] = fHDHR_API(fhdhr)
self.fhdhr.logger.info("Loading HTTP API Endpoints.") self.selfadd_web_plugins()
self.api = fHDHR_API(fhdhr) for endpoint_type in list(self.endpoints_obj.keys()):
self.add_endpoints(self.api, "api") self.fhdhr.logger.info("Loading HTTP %s Endpoints." % endpoint_type)
self.add_endpoints(endpoint_type)
self.app.before_request(self.before_request) self.fhdhr.app.before_request(self.before_request)
self.app.after_request(self.after_request) self.fhdhr.app.after_request(self.after_request)
self.app.before_first_request(self.before_first_request) self.fhdhr.app.before_first_request(self.before_first_request)
self.fhdhr.threads["flask"] = threading.Thread(target=self.run)
def selfadd_web_plugins(self):
for plugin_name in list(self.fhdhr.plugins.plugins.keys()):
if self.fhdhr.plugins.plugins[plugin_name].type == "web":
method = self.fhdhr.plugins.plugins[plugin_name].name.lower()
plugin_utils = self.fhdhr.plugins.plugins[plugin_name].plugin_utils
try:
self.endpoints_obj[method] = self.fhdhr.plugins.plugins[plugin_name].Plugin_OBJ(self.fhdhr, plugin_utils)
except Exception as e:
print(e)
def start(self):
self.fhdhr.logger.info("Flask HTTP Thread Starting")
self.fhdhr.threads["flask"].start()
def stop(self):
self.fhdhr.logger.info("Flask HTTP Thread Stopping")
self.http.stop()
def before_first_request(self): def before_first_request(self):
self.fhdhr.logger.info("HTTP Server Online.") self.fhdhr.logger.info("HTTP Server Online.")
def before_request(self): def before_request(self):
session["session_id"] = str(uuid.uuid4())
session["instance_id"] = self.instance_id
session["route_list"] = self.route_list
session["user_agent"] = request.headers.get('User-Agent')
session["is_internal_api"] = self.detect_internal_api(request)
if session["is_internal_api"]:
self.fhdhr.logger.debug("Client is using internal API call.")
session["is_mobile"] = self.detect_mobile(request)
if session["is_mobile"]:
self.fhdhr.logger.debug("Client is a mobile device.")
session["is_plexmediaserver"] = self.detect_plexmediaserver(request)
if session["is_plexmediaserver"]:
self.fhdhr.logger.debug("Client is a Plex Media Server.")
session["deviceauth"] = self.detect_plexmediaserver(request)
session["tuner_used"] = None
session["restart"] = False
self.fhdhr.logger.debug("Client %s requested %s Opening" % (request.method, request.path)) self.fhdhr.logger.debug("Client %s requested %s Opening" % (request.method, request.path))
def after_request(self, response): def after_request(self, response):
self.fhdhr.logger.debug("Client %s requested %s Closing" % (request.method, request.path))
return response
def add_endpoints(self, index_list, index_name): # Close Tuner if it was in use, and did not close already
item_list = [x for x in dir(index_list) if self.isapath(x)] # if session["tuner_used"] is not None:
# tuner = self.fhdhr.device.tuners.tuners[str(session["tuner_used"])]
# if tuner.tuner_lock.locked():
# self.fhdhr.logger.info("Shutting down Tuner #%s after Request." % session["tuner_used"])
# tuner.close()
self.fhdhr.logger.debug("Client %s requested %s Closing" % (request.method, request.path))
if not session["restart"]:
return response
else:
return self.stop()
def detect_internal_api(self, request):
user_agent = request.headers.get('User-Agent')
if not user_agent:
return False
elif str(user_agent).lower().startswith("fhdhr"):
return True
else:
return False
def detect_deviceauth(self, request):
return request.args.get('DeviceAuth', default=None, type=str)
def detect_mobile(self, request):
user_agent = request.headers.get('User-Agent')
phones = ["iphone", "android", "blackberry"]
if not user_agent:
return False
elif any(phone in user_agent.lower() for phone in phones):
return True
else:
return False
def detect_plexmediaserver(self, request):
user_agent = request.headers.get('User-Agent')
if not user_agent:
return False
elif str(user_agent).lower().startswith("plexmediaserver"):
return True
else:
return False
def add_endpoints(self, index_name):
item_list = [x for x in dir(self.endpoints_obj[index_name]) if self.isapath(x)]
endpoint_main = self.endpoints_obj[index_name]
endpoint_main.fhdhr.version # dummy line
for item in item_list: for item in item_list:
endpoints = eval("self." + str(index_name) + "." + str(item) + ".endpoints") endpoints = eval("endpoint_main.%s.%s" % (item, "endpoints"))
if isinstance(endpoints, str): if isinstance(endpoints, str):
endpoints = [endpoints] endpoints = [endpoints]
handler = eval("self." + str(index_name) + "." + str(item)) handler = eval("endpoint_main.%s" % item)
endpoint_name = eval("self." + str(index_name) + "." + str(item) + ".endpoint_name") endpoint_name = eval("endpoint_main.%s.%s" % (item, "endpoint_name"))
try: try:
endpoint_methods = eval("self." + str(index_name) + "." + str(item) + ".endpoint_methods") endpoint_methods = eval("endpoint_main.%s.%s" % (item, "endpoint_methods"))
except AttributeError: except AttributeError:
endpoint_methods = ['GET'] endpoint_methods = ['GET']
self.fhdhr.logger.info("Adding endpoint %s available at %s with %s methods." % (endpoint_name, ",".join(endpoints), ",".join(endpoint_methods)))
try:
endpoint_access_level = eval("endpoint_main.%s.%s" % (item, "endpoint_access_level"))
except AttributeError:
endpoint_access_level = 0
try:
pretty_name = eval("endpoint_main.%s.%s" % (item, "pretty_name"))
except AttributeError:
pretty_name = endpoint_name
try:
endpoint_category = eval("endpoint_main.%s.%s" % (item, "endpoint_category"))
except AttributeError:
endpoint_category = index_name
try:
endpoint_default_parameters = eval("endpoint_main.%s.%s" % (item, "endpoint_default_parameters"))
except AttributeError:
endpoint_default_parameters = {}
self.fhdhr.logger.debug("Adding endpoint %s available at %s with %s methods." % (endpoint_name, ",".join(endpoints), ",".join(endpoint_methods)))
if endpoint_category not in list(self.route_list.keys()):
self.route_list[endpoint_category] = {}
if endpoint_name not in list(self.route_list[endpoint_category].keys()):
self.route_list[endpoint_category][endpoint_name] = {}
self.route_list[endpoint_category][endpoint_name]["name"] = endpoint_name
self.route_list[endpoint_category][endpoint_name]["endpoints"] = endpoints
self.route_list[endpoint_category][endpoint_name]["endpoint_methods"] = endpoint_methods
self.route_list[endpoint_category][endpoint_name]["endpoint_access_level"] = endpoint_access_level
self.route_list[endpoint_category][endpoint_name]["endpoint_default_parameters"] = endpoint_default_parameters
self.route_list[endpoint_category][endpoint_name]["pretty_name"] = pretty_name
self.route_list[endpoint_category][endpoint_name]["endpoint_category"] = endpoint_category
for endpoint in endpoints: for endpoint in endpoints:
self.add_endpoint(endpoint=endpoint, self.add_endpoint(endpoint=endpoint,
endpoint_name=endpoint_name, endpoint_name=endpoint_name,
@ -77,7 +205,7 @@ class fHDHR_HTTP_Server():
methods=endpoint_methods) methods=endpoint_methods)
def isapath(self, item): def isapath(self, item):
not_a_page_list = ["fhdhr", "htmlerror", "page_elements"] not_a_page_list = ["fhdhr", "plugin_utils"]
if item in not_a_page_list: if item in not_a_page_list:
return False return False
elif item.startswith("__") and item.endswith("__"): elif item.startswith("__") and item.endswith("__"):
@ -86,15 +214,16 @@ class fHDHR_HTTP_Server():
return True return True
def add_endpoint(self, endpoint=None, endpoint_name=None, handler=None, methods=['GET']): def add_endpoint(self, endpoint=None, endpoint_name=None, handler=None, methods=['GET']):
self.app.add_url_rule(endpoint, endpoint_name, handler, methods=methods) self.fhdhr.app.add_url_rule(endpoint, endpoint_name, handler, methods=methods)
def run(self): def run(self):
self.http = WSGIServer(self.fhdhr.api.address_tuple, self.http = WSGIServer(self.fhdhr.api.address_tuple,
self.app.wsgi_app, self.fhdhr.app.wsgi_app,
log=self.fhdhr.logger) log=self.fhdhr.logger.logger,
error_log=self.fhdhr.logger.logger)
try: try:
self.http.serve_forever() self.http.serve_forever()
except KeyboardInterrupt: self.stop()
self.http.stop() except AttributeError:
self.fhdhr.logger.info("HTTP Server Offline")

View File

@ -1,14 +1,18 @@
from .root_url import Root_URL from .root_url import Root_URL
from .startup_tasks import Startup_Tasks
from .cluster import Cluster
from .settings import Settings from .settings import Settings
from .channels import Channels from .channels import Channels
from .xmltv import xmlTV from .xmltv import xmlTV
from .m3u import M3U from .m3u import M3U
from .w3u import W3U
from .epg import EPG from .epg import EPG
from .tuners import Tuners from .tuners import Tuners
from .debug import Debug_JSON from .debug import Debug_JSON
from .plugins import Plugins_JSON
from .route_list import Route_List
from .images import Images from .images import Images
@ -19,14 +23,18 @@ class fHDHR_API():
self.fhdhr = fhdhr self.fhdhr = fhdhr
self.root_url = Root_URL(fhdhr) self.root_url = Root_URL(fhdhr)
self.startup_tasks = Startup_Tasks(fhdhr)
self.cluster = Cluster(fhdhr)
self.settings = Settings(fhdhr) self.settings = Settings(fhdhr)
self.channels = Channels(fhdhr) self.channels = Channels(fhdhr)
self.xmltv = xmlTV(fhdhr) self.xmltv = xmlTV(fhdhr)
self.m3u = M3U(fhdhr) self.m3u = M3U(fhdhr)
self.w3u = W3U(fhdhr)
self.epg = EPG(fhdhr) self.epg = EPG(fhdhr)
self.tuners = Tuners(fhdhr) self.tuners = Tuners(fhdhr)
self.debug = Debug_JSON(fhdhr) self.debug = Debug_JSON(fhdhr)
self.plugins = Plugins_JSON(fhdhr)
self.route_list = Route_List(fhdhr)
self.images = Images(fhdhr) self.images = Images(fhdhr)

View File

@ -2,11 +2,16 @@ from flask import request, redirect, Response, abort
import urllib.parse import urllib.parse
import json import json
from fHDHR.tools import channel_sort
class Channels(): class Channels():
endpoints = ["/api/channels"] endpoints = ["/api/channels"]
endpoint_name = "api_channels" endpoint_name = "api_channels"
endpoint_methods = ["GET", "POST"] endpoint_methods = ["GET", "POST"]
endpoint_default_parameters = {
"method": "get"
}
def __init__(self, fhdhr): def __init__(self, fhdhr):
self.fhdhr = fhdhr self.fhdhr = fhdhr
@ -19,14 +24,37 @@ class Channels():
method = request.args.get('method', default=None, type=str) method = request.args.get('method', default=None, type=str)
redirect_url = request.args.get('redirect', default=None, type=str) redirect_url = request.args.get('redirect', default=None, type=str)
origin_methods = self.fhdhr.origins.valid_origins
origin = request.args.get('origin', default=None, type=str)
if origin and origin not in origin_methods:
return "%s Invalid channels origin" % origin
if method == "get": if method == "get":
channels_info = [] channels_info = {}
for fhdhr_id in list(self.fhdhr.device.channels.list.keys()): if not origin:
channel_obj = self.fhdhr.device.channels.list[fhdhr_id] origin_list = origin_methods
else:
origin_list = [origin]
for origin_item in origin_list:
channels_info[origin_item] = {}
for fhdhr_id in [x["id"] for x in self.fhdhr.device.channels.get_channels(origin=origin_item)]:
channel_obj = self.fhdhr.device.channels.list[origin_item][fhdhr_id]
channel_dict = channel_obj.dict.copy() channel_dict = channel_obj.dict.copy()
channel_dict["play_url"] = channel_obj.play_url channel_dict["m3u_url"] = channel_obj.api_m3u_url
channel_dict["stream_url"] = channel_obj.stream_url channel_dict["stream_url"] = channel_obj.api_stream_url
channels_info.append(channel_dict) channels_info[origin_item][channel_obj.number] = channel_dict
# Sort the channels
sorted_channel_list = channel_sort(list(channels_info[origin_item].keys()))
sorted_chan_guide = []
for channel in sorted_channel_list:
sorted_chan_guide.append(channels_info[origin_item][channel])
channels_info[origin_item] = sorted_chan_guide
channels_info_json = json.dumps(channels_info, indent=4) channels_info_json = json.dumps(channels_info, indent=4)
return Response(status=200, return Response(status=200,
@ -38,7 +66,7 @@ class Channels():
channel = request.args.get('channel', default=None, type=str) channel = request.args.get('channel', default=None, type=str)
if not channel: if not channel:
if redirect_url: if redirect_url:
return redirect(redirect_url + "?retmessage=" + urllib.parse.quote("%s Failed" % method)) return redirect("%s?retmessage=%s" % (redirect_url, urllib.parse.quote("%s Failed" % method)))
else: else:
return "%s Falied" % method return "%s Falied" % method
@ -47,34 +75,34 @@ class Channels():
channel_method = channel[0] channel_method = channel[0]
channel_number = channel[1:] channel_number = channel[1:]
if str(channel_number) not in [str(x) for x in self.fhdhr.device.channels.get_channel_list("number")]: if str(channel_number) not in [str(x) for x in self.fhdhr.device.channels.get_channel_list("number", origin)]:
response = Response("Not Found", status=404) response = Response("Not Found", status=404)
response.headers["X-fHDHR-Error"] = "801 - Unknown Channel" response.headers["X-fHDHR-Error"] = "801 - Unknown Channel"
self.fhdhr.logger.error(response.headers["X-fHDHR-Error"]) self.fhdhr.logger.error(response.headers["X-fHDHR-Error"])
abort(response) abort(response)
if channel_method == "+": if channel_method == "+":
self.fhdhr.device.channels.set_channel_enablement("number", channel_number, channel_method) self.fhdhr.device.channels.set_channel_enablement("number", channel_number, channel_method, origin)
elif channel_method == "-": elif channel_method == "-":
self.fhdhr.device.channels.set_channel_enablement("number", channel_number, channel_method) self.fhdhr.device.channels.set_channel_enablement("number", channel_number, channel_method, origin)
elif channel_method == "x": elif channel_method == "x":
self.fhdhr.device.channels.set_channel_enablement("number", channel_number, "toggle") self.fhdhr.device.channels.set_channel_enablement("number", channel_number, "toggle", origin)
else: else:
self.fhdhr.logger.warning("Unknown favorite command " + request.args['favorite']) self.fhdhr.logger.warning("Unknown favorite command %s" % request.args['favorite'])
return abort(200, "Not a valid favorite command") return abort(200, "Not a valid favorite command")
elif method in ["enable", "disable"]: elif method in ["enable", "disable"]:
channel = request.args.get('channel', default=None, type=str) channel = request.args.get('channel', default=None, type=str)
if channel == "all": if channel == "all":
self.fhdhr.device.channels.set_channel_enablement_all(method) self.fhdhr.device.channels.set_channel_enablement_all(method, origin)
elif not channel or str(channel) not in [str(x) for x in self.fhdhr.device.channels.get_channel_list("number")]: elif not channel or str(channel) not in [str(x) for x in self.fhdhr.device.channels.get_channel_list("number", origin)]:
if redirect_url: if redirect_url:
return redirect(redirect_url + "?retmessage=" + urllib.parse.quote("%s Failed" % method)) return redirect("%s?retmessage=%s" % (redirect_url, urllib.parse.quote("%s Failed" % method)))
else: else:
return "%s Falied" % method return "%s Falied" % method
else: else:
self.fhdhr.device.channels.set_channel_enablement("number", channel, method) self.fhdhr.device.channels.set_channel_enablement("number", channel, method, origin)
elif method == "update": elif method == "update":
channel_id = request.form.get('id', None) channel_id = request.form.get('id', None)
@ -84,7 +112,12 @@ class Channels():
if key in ["name", "callsign", "thumbnail"]: if key in ["name", "callsign", "thumbnail"]:
updatedict[key] = str(request.form.get(key)) updatedict[key] = str(request.form.get(key))
elif key in ["number"]: elif key in ["number"]:
updatedict[key] = float(request.form.get(key)) number = str(request.form.get(key))
if "." in number:
updatedict["subnumber"] = number.split(".")[1]
updatedict["number"] = number.split(".")[0]
else:
updatedict["number"] = number
elif key in ["enabled"]: elif key in ["enabled"]:
confvalue = request.form.get(key) confvalue = request.form.get(key)
if str(confvalue).lower() in ["false"]: if str(confvalue).lower() in ["false"]:
@ -94,16 +127,44 @@ class Channels():
updatedict[key] = confvalue updatedict[key] = confvalue
elif key in ["favorite", "HD"]: elif key in ["favorite", "HD"]:
updatedict[key] = int(request.form.get(key)) updatedict[key] = int(request.form.get(key))
self.fhdhr.device.channels.set_channel_status("id", channel_id, updatedict) self.fhdhr.device.channels.set_channel_status("id", channel_id, updatedict, origin)
elif method == "modify":
channels_list = json.loads(request.form.get('channels', []))
for channel in channels_list:
updatedict = {}
for key in list(channel.keys()):
if key != "id":
if key in ["name", "callsign", "thumbnail"]:
updatedict[key] = str(channel[key])
elif key in ["number"]:
number = str(channel[key])
if "." in number:
updatedict["subnumber"] = number.split(".")[1]
updatedict["number"] = number.split(".")[0]
else:
updatedict["number"] = number
elif key in ["enabled"]:
confvalue = channel[key]
if str(confvalue).lower() in ["false"]:
confvalue = False
elif str(confvalue).lower() in ["true"]:
confvalue = True
updatedict[key] = confvalue
elif key in ["favorite", "HD"]:
updatedict[key] = int(channel[key])
else:
channel_id = str(channel[key])
self.fhdhr.device.channels.set_channel_status("id", channel_id, updatedict, origin)
elif method == "scan": elif method == "scan":
self.fhdhr.device.channels.get_channels(forceupdate=True) self.fhdhr.device.channels.get_channels(forceupdate=True, origin=origin)
else: else:
return "Invalid Method" return "Invalid Method"
if redirect_url: if redirect_url:
return redirect(redirect_url + "?retmessage=" + urllib.parse.quote("%s Success" % method)) return redirect("%s?retmessage=%s" % (redirect_url, urllib.parse.quote("%s Success" % method)))
else: else:
if method == "scan": if method == "scan":
return redirect('/lineup_status.json') return redirect('/lineup_status.json')

View File

@ -1,56 +0,0 @@
from flask import request, redirect, Response
import urllib.parse
import json
class Cluster():
endpoints = ["/api/cluster"]
endpoint_name = "api_cluster"
endpoint_methods = ["GET", "POST"]
def __init__(self, fhdhr):
self.fhdhr = fhdhr
def __call__(self, *args):
return self.get(*args)
def get(self, *args):
method = request.args.get('method', default="get", type=str)
location = request.args.get("location", default=None, type=str)
redirect_url = request.args.get('redirect', default=None, type=str)
if method == "get":
jsoncluster = self.fhdhr.device.cluster.cluster()
cluster_json = json.dumps(jsoncluster, indent=4)
return Response(status=200,
response=cluster_json,
mimetype='application/json')
elif method == "scan":
self.fhdhr.device.ssdp.m_search()
elif method == 'add':
self.fhdhr.device.cluster.add(location)
elif method == 'del':
self.fhdhr.device.cluster.remove(location)
elif method == 'sync':
self.fhdhr.device.cluster.sync(location)
elif method == 'leave':
self.fhdhr.device.cluster.leave()
elif method == 'disconnect':
self.fhdhr.device.cluster.disconnect()
elif method == 'alive':
self.fhdhr.device.ssdp.do_alive(forcealive=True)
else:
return "Invalid Method"
if redirect_url:
return redirect(redirect_url + "?retmessage=" + urllib.parse.quote("%s Success" % method))
else:
return "%s Success" % method

View File

@ -19,11 +19,16 @@ class Debug_JSON():
debugjson = { debugjson = {
"base_url": base_url, "base_url": base_url,
"total channels": len(self.fhdhr.device.channels.list),
"tuner status": self.fhdhr.device.tuners.status(),
} }
cluster_json = json.dumps(debugjson, indent=4)
for origin in list(self.fhdhr.origins.origins_dict.keys()):
debugjson[origin] = {
"tuner status": self.fhdhr.device.tuners.status(origin),
"total channels": len(list(self.fhdhr.device.channels.list[origin].keys()))
}
debug_json = json.dumps(debugjson, indent=4)
return Response(status=200, return Response(status=200,
response=cluster_json, response=debug_json,
mimetype='application/json') mimetype='application/json')

View File

@ -1,6 +1,9 @@
from flask import Response, request, redirect from flask import Response, request, redirect
import urllib.parse import urllib.parse
import json import json
import datetime
from fHDHR.tools import humanized_time, channel_sort
class EPG(): class EPG():
@ -20,25 +23,95 @@ class EPG():
method = request.args.get('method', default="get", type=str) method = request.args.get('method', default="get", type=str)
source = request.args.get('source', default=self.fhdhr.config.dict["epg"]["def_method"], type=str) source = request.args.get('source', default=self.fhdhr.config.dict["epg"]["def_method"], type=str)
if source not in self.fhdhr.config.dict["main"]["valid_epg_methods"]: if source not in list(self.fhdhr.config.dict["epg"]["valid_methods"].keys()):
return "%s Invalid xmltv method" % source return "%s Invalid epg method" % source
redirect_url = request.args.get('redirect', default=None, type=str) redirect_url = request.args.get('redirect', default=None, type=str)
if method == "get": if method == "get":
epgdict = self.fhdhr.device.epg.get_epg(source) epgdict = self.fhdhr.device.epg.get_epg(source)
if source in self.fhdhr.origins.valid_origins:
epgdict = epgdict.copy() epgdict = epgdict.copy()
for c in list(epgdict.keys()): for c in list(epgdict.keys()):
chan_obj = self.fhdhr.device.channels.get_channel_obj("origin_id", epgdict[c]["id"]) chan_obj = self.fhdhr.device.channels.get_channel_obj("origin_id", epgdict[c]["id"], source)
epgdict[chan_obj.dict["number"]] = epgdict.pop(c) epgdict[chan_obj.number] = epgdict.pop(c)
epgdict[chan_obj.dict["number"]]["name"] = chan_obj.dict["name"] epgdict[chan_obj.number]["name"] = chan_obj.dict["name"]
epgdict[chan_obj.dict["number"]]["callsign"] = chan_obj.dict["callsign"] epgdict[chan_obj.number]["callsign"] = chan_obj.dict["callsign"]
epgdict[chan_obj.dict["number"]]["number"] = chan_obj.dict["number"] epgdict[chan_obj.number]["number"] = chan_obj.number
epgdict[chan_obj.dict["number"]]["id"] = chan_obj.dict["origin_id"] epgdict[chan_obj.number]["id"] = chan_obj.dict["origin_id"]
epgdict[chan_obj.dict["number"]]["thumbnail"] = chan_obj.thumbnail epgdict[chan_obj.number]["thumbnail"] = chan_obj.thumbnail
epg_json = json.dumps(epgdict, indent=4) # Sort the channels
sorted_channel_list = channel_sort(list(epgdict.keys()))
sorted_chan_guide = {}
for channel in sorted_channel_list:
sorted_chan_guide[channel] = epgdict[channel]
epg_json = json.dumps(sorted_chan_guide, indent=4)
return Response(status=200,
response=epg_json,
mimetype='application/json')
elif method == "current":
nowtime = datetime.datetime.utcnow().timestamp()
chan_guide_list = []
whatson = self.fhdhr.device.epg.whats_on_allchans(source)
# Sort the channels
sorted_channel_list = channel_sort(list(whatson.keys()))
sorted_chan_guide = {}
for channel in sorted_channel_list:
sorted_chan_guide[channel] = whatson[channel]
for channel in list(sorted_chan_guide.keys()):
if sorted_chan_guide[channel]["listing"][0]["time_end"]:
remaining_time = humanized_time(sorted_chan_guide[channel]["listing"][0]["time_end"] - nowtime)
else:
remaining_time = "N/A"
chan_dict = {
"name": sorted_chan_guide[channel]["name"],
"number": sorted_chan_guide[channel]["number"],
"chan_thumbnail": sorted_chan_guide[channel]["thumbnail"],
"listing_title": sorted_chan_guide[channel]["listing"][0]["title"],
"listing_thumbnail": sorted_chan_guide[channel]["listing"][0]["thumbnail"],
"listing_description": sorted_chan_guide[channel]["listing"][0]["description"],
"listing_remaining_time": str(remaining_time)
}
for time_item in ["time_start", "time_end"]:
if not sorted_chan_guide[channel]["listing"][0][time_item]:
chan_dict["listing_%s" % time_item] = "N/A"
elif str(sorted_chan_guide[channel]["listing"][0][time_item]).endswith(tuple(["+0000", "+00:00"])):
chan_dict["listing_%s" % time_item] = str(sorted_chan_guide[channel]["listing"][0][time_item])
else:
chan_dict["listing_%s" % time_item] = str(datetime.datetime.fromtimestamp(sorted_chan_guide[channel]["listing"][0][time_item]))
if source in self.fhdhr.origins.valid_origins:
chan_obj = self.fhdhr.device.channels.get_channel_obj("origin_id", sorted_chan_guide[channel]["id"], source)
chan_dict["name"] = chan_obj.dict["name"]
chan_dict["number"] = chan_obj.number
chan_dict["chan_thumbnail"] = chan_obj.thumbnail
chan_dict["enabled"] = chan_obj.dict["enabled"]
chan_dict["m3u_url"] = chan_obj.api_m3u_url
chan_dict["listing_thumbnail"] = chan_dict["listing_thumbnail"] or chan_obj.thumbnail
else:
if not chan_dict["listing_thumbnail"]:
chan_dict["listing_thumbnail"] = chan_dict["chan_thumbnail"]
if not chan_dict["listing_thumbnail"]:
chan_dict["listing_thumbnail"] = "/api/images?method=generate&type=channel&message=%s" % chan_dict["number"]
chan_guide_list.append(chan_dict)
epg_json = json.dumps(chan_guide_list, indent=4)
return Response(status=200, return Response(status=200,
response=epg_json, response=epg_json,
@ -54,6 +127,6 @@ class EPG():
return "%s Invalid Method" % method return "%s Invalid Method" % method
if redirect_url: if redirect_url:
return redirect(redirect_url + "?retmessage=" + urllib.parse.quote("%s Success" % method)) return redirect("%s?retmessage=%s" % (redirect_url, urllib.parse.quote("%s Success" % method)))
else: else:
return "%s Success" % method return "%s Success" % method

View File

@ -5,6 +5,11 @@ class Images():
endpoints = ["/api/images"] endpoints = ["/api/images"]
endpoint_name = "api_images" endpoint_name = "api_images"
endpoint_methods = ["GET", "POST"] endpoint_methods = ["GET", "POST"]
endpoint_default_parameters = {
"method": "generate",
"type": "content",
"message": "Internal Image Handling"
}
def __init__(self, fhdhr): def __init__(self, fhdhr):
self.fhdhr = fhdhr self.fhdhr = fhdhr
@ -26,7 +31,7 @@ class Images():
elif method == "get": elif method == "get":
source = request.args.get('source', default=self.fhdhr.config.dict["epg"]["method"], type=str) source = request.args.get('source', default=self.fhdhr.config.dict["epg"]["method"], type=str)
if source in self.fhdhr.config.dict["main"]["valid_epg_methods"]: if source in list(self.fhdhr.config.dict["epg"]["valid_methods"].keys()):
image_type = request.args.get('type', default="content", type=str) image_type = request.args.get('type', default="content", type=str)
if image_type in ["content", "channel"]: if image_type in ["content", "channel"]:
image_id = request.args.get('id', default=None, type=str) image_id = request.args.get('id', default=None, type=str)

View File

@ -2,6 +2,8 @@ from flask import Response, request, redirect
import urllib.parse import urllib.parse
from io import StringIO from io import StringIO
from fHDHR.tools import channel_sort
class M3U(): class M3U():
endpoints = ["/api/m3u", "/api/channels.m3u"] endpoints = ["/api/m3u", "/api/channels.m3u"]
@ -24,6 +26,11 @@ class M3U():
if method == "get": if method == "get":
origin_methods = self.fhdhr.origins.valid_origins
origin = request.args.get('origin', default=None, type=str)
if origin and origin not in origin_methods:
return "%s Invalid channels origin" % origin
FORMAT_DESCRIPTOR = "#EXTM3U" FORMAT_DESCRIPTOR = "#EXTM3U"
RECORD_MARKER = "#EXTINF" RECORD_MARKER = "#EXTINF"
@ -31,24 +38,41 @@ class M3U():
xmltvurl = ('%s/api/xmltv' % base_url) xmltvurl = ('%s/api/xmltv' % base_url)
fakefile.write( fakefile.write("%s url-tvg=\"%s\" x-tvg-url=\"%s\"\n" % (FORMAT_DESCRIPTOR, xmltvurl, xmltvurl))
"%s\n" % (
FORMAT_DESCRIPTOR + " " +
"url-tvg=\"" + xmltvurl + "\"" + " " +
"x-tvg-url=\"" + xmltvurl + "\"")
)
channel_items = [] channel_items = []
if origin:
if channel == "all": if channel == "all":
fileName = "channels.m3u" fileName = "channels.m3u"
for fhdhr_id in list(self.fhdhr.device.channels.list.keys()): for fhdhr_id in [x["id"] for x in self.fhdhr.device.channels.get_channels(origin)]:
channel_obj = self.fhdhr.device.channels.list[fhdhr_id] channel_obj = self.fhdhr.device.channels.get_channel_obj("id", fhdhr_id, origin)
if channel_obj.enabled: if channel_obj.enabled:
channel_items.append(channel_obj) channel_items.append(channel_obj)
elif str(channel) in [str(x) for x in self.fhdhr.device.channels.get_channel_list("number")]: elif str(channel) in [str(x) for x in self.fhdhr.device.channels.get_channel_list("number", origin)]:
channel_obj = self.fhdhr.device.channels.get_channel_obj("number", channel) channel_obj = self.fhdhr.device.channels.get_channel_obj("number", channel, origin)
fileName = str(channel_obj.number) + ".m3u" fileName = "%s.m3u" % channel_obj.number
if channel_obj.enabled:
channel_items.append(channel_obj)
else:
return "Channel Disabled"
elif channel != "all" and str(channel) in [str(x) for x in self.fhdhr.device.channels.get_channel_list("id", origin)]:
channel_obj = self.fhdhr.device.channels.get_channel_obj("id", channel, origin)
fileName = "%s.m3u" % channel_obj.number
if channel_obj.enabled:
channel_items.append(channel_obj)
else:
return "Channel Disabled"
elif not origin and channel == "all":
fileName = "channels.m3u"
for origin in list(self.fhdhr.origins.origins_dict.keys()):
for fhdhr_id in [x["id"] for x in self.fhdhr.device.channels.get_channels(origin)]:
channel_obj = self.fhdhr.device.channels.get_channel_obj("id", fhdhr_id, origin)
if channel_obj.enabled:
channel_items.append(channel_obj)
elif not origin and channel != "all" and str(channel) in [str(x) for x in self.fhdhr.device.channels.get_channel_list("id")]:
channel_obj = self.fhdhr.device.channels.get_channel_obj("id", channel)
fileName = "%s.m3u" % channel_obj.number
if channel_obj.enabled: if channel_obj.enabled:
channel_items.append(channel_obj) channel_items.append(channel_obj)
else: else:
@ -56,6 +80,7 @@ class M3U():
else: else:
return "Invalid Channel" return "Invalid Channel"
channels_info = {}
for channel_obj in channel_items: for channel_obj in channel_items:
if self.fhdhr.config.dict["epg"]["images"] == "proxy" or not channel_obj.thumbnail: if self.fhdhr.config.dict["epg"]["images"] == "proxy" or not channel_obj.thumbnail:
@ -64,26 +89,39 @@ class M3U():
else: else:
logourl = channel_obj.thumbnail logourl = channel_obj.thumbnail
fakefile.write( channels_info[channel_obj.number] = {
"%s\n" % ( "channelID": str(channel_obj.dict['origin_id']),
RECORD_MARKER + ":0" + " " + "tvg-chno": str(channel_obj.number),
"channelID=\"" + str(channel_obj.dict['origin_id']) + "\" " + "tvg-name": str(channel_obj.dict['name']),
"tvg-chno=\"" + str(channel_obj.dict['number']) + "\" " + "tvg-id": str(channel_obj.number),
"tvg-name=\"" + str(channel_obj.dict['name']) + "\" " + "tvg-logo": logourl,
"tvg-id=\"" + str(channel_obj.dict['number']) + "\" " + "group-title": channel_obj.origin,
"tvg-logo=\"" + logourl + "\" " + "group-titleb": str(channel_obj.dict['name']),
"group-title=\"" + self.fhdhr.config.dict["fhdhr"]["friendlyname"] + "\"," + str(channel_obj.dict['name'])) "stream_url": "%s%s" % (base_url, channel_obj.api_stream_url)
) }
fakefile.write("%s%s\n" % (base_url, channel_obj.stream_url)) # Sort the channels
sorted_channel_list = channel_sort(list(channels_info.keys()))
sorted_chan_guide = []
for channel in sorted_channel_list:
sorted_chan_guide.append(channels_info[channel])
for channel_item_dict in sorted_chan_guide:
m3ustring = "%s:0 " % (RECORD_MARKER)
for chan_key in list(channel_item_dict.keys()):
if not chan_key.startswith(tuple(["group-title", "stream_url"])):
m3ustring += "%s=\"%s\" " % (chan_key, channel_item_dict[chan_key])
m3ustring += "group-title=\"%s\",%s\n" % (channel_item_dict["group-title"], channel_item_dict["group-titleb"])
m3ustring += "%s\n" % channel_item_dict["stream_url"]
fakefile.write(m3ustring)
channels_m3u = fakefile.getvalue() channels_m3u = fakefile.getvalue()
resp = Response(status=200, response=channels_m3u, mimetype='audio/x-mpegurl') resp = Response(status=200, response=channels_m3u, mimetype='audio/x-mpegurl')
resp.headers["content-disposition"] = "attachment; filename=" + fileName resp.headers["content-disposition"] = "attachment; filename=%s" % fileName
return resp return resp
if redirect_url: if redirect_url:
return redirect(redirect_url + "?retmessage=" + urllib.parse.quote("%s Success" % method)) return redirect("%s?retmessage=%s" % (redirect_url, urllib.parse.quote("%s Success" % method)))
else: else:
return "%s Success" % method return "%s Success" % method

30
fHDHR_web/api/plugins.py Normal file
View File

@ -0,0 +1,30 @@
from flask import Response
import json
class Plugins_JSON():
endpoints = ["/api/plugins"]
endpoint_name = "api_plugins"
endpoint_methods = ["GET", "POST"]
def __init__(self, fhdhr):
self.fhdhr = fhdhr
def __call__(self, *args):
return self.get(*args)
def get(self, *args):
pluginsjson = {}
for plugin in list(self.fhdhr.plugins.plugins.keys()):
pluginsjson[plugin] = {
"name": plugin,
"manifest": self.fhdhr.plugins.plugins[plugin].manifest
}
plugins_json = json.dumps(pluginsjson, indent=4)
return Response(status=200,
response=plugins_json,
mimetype='application/json')

View File

@ -1,4 +1,4 @@
from flask import redirect, request from flask import redirect
class Root_URL(): class Root_URL():
@ -13,20 +13,4 @@ class Root_URL():
return self.get(*args) return self.get(*args)
def get(self, *args): def get(self, *args):
user_agent = request.headers.get('User-Agent')
# Client Devices Discovering Device Information
if not user_agent or str(user_agent).lower().startswith("plexmediaserver"):
# Plex Remote Media Grabber redirect
if self.fhdhr.config.dict["rmg"]["enabled"] and str(user_agent).lower().startswith("plexmediaserver"):
return redirect("/rmg")
# Client Device is looking for HDHR type device
else:
return redirect("/hdhr/device.xml")
# Anything Else is likely a Web Browser
else:
return redirect("/index") return redirect("/index")

View File

@ -0,0 +1,37 @@
from flask import Response, request, redirect, session
import urllib.parse
import json
class Route_List():
endpoints = ["/api/routes"]
endpoint_name = "api_routes"
endpoint_methods = ["GET", "POST"]
def __init__(self, fhdhr):
self.fhdhr = fhdhr
def __call__(self, *args):
return self.get(*args)
def get(self, *args):
method = request.args.get('method', default="get", type=str)
redirect_url = request.args.get('redirect', default=None, type=str)
if method == "get":
return_json = json.dumps(session["route_list"], indent=4)
return Response(status=200,
response=return_json,
mimetype='application/json')
else:
return "%s Invalid Method" % method
if redirect_url:
return redirect("%s?retmessage=%s" % (redirect_url, urllib.parse.quote("%s Success" % method)))
else:
return "%s Success" % method

View File

@ -1,5 +1,8 @@
from flask import request, redirect from flask import request, redirect, Response, session
import urllib.parse import urllib.parse
import threading
import time
import json
class Settings(): class Settings():
@ -10,6 +13,9 @@ class Settings():
def __init__(self, fhdhr): def __init__(self, fhdhr):
self.fhdhr = fhdhr self.fhdhr = fhdhr
self.restart_url = "/api/settings?method=restart_actual"
self.restart_sleep = 5
def __call__(self, *args): def __call__(self, *args):
return self.get(*args) return self.get(*args)
@ -18,23 +24,50 @@ class Settings():
method = request.args.get('method', default="get", type=str) method = request.args.get('method', default="get", type=str)
redirect_url = request.args.get('redirect', default=None, type=str) redirect_url = request.args.get('redirect', default=None, type=str)
if method == "update": if method == "get":
web_settings_dict = {}
for config_section in list(self.fhdhr.config.conf_default.keys()):
web_settings_dict[config_section] = {}
for config_item in list(self.fhdhr.config.conf_default[config_section].keys()):
web_settings_dict[config_section][config_item] = {
"value": self.fhdhr.config.dict[config_section][config_item],
}
if self.fhdhr.config.conf_default[config_section][config_item]["config_web_hidden"]:
web_settings_dict[config_section][config_item]["value"] = "***********"
return_json = json.dumps(web_settings_dict, indent=4)
return Response(status=200,
response=return_json,
mimetype='application/json')
elif method == "update":
config_section = request.form.get('config_section', None) config_section = request.form.get('config_section', None)
config_name = request.form.get('config_name', None) config_name = request.form.get('config_name', None)
config_value = request.form.get('config_value', None) config_value = request.form.get('config_value', None)
if not config_section or not config_name or not config_value: if not config_section or not config_name or not config_value:
if redirect_url: if redirect_url:
return redirect(redirect_url + "?retmessage=" + urllib.parse.quote("%s Failed" % method)) return redirect("%s?retmessage=%s" % (redirect_url, urllib.parse.quote("%s Failed" % method)))
else: else:
return "%s Falied" % method return "%s Falied" % method
if config_section == "origin": self.fhdhr.config.write(config_name, config_value, config_section)
config_section = self.fhdhr.config.dict["main"]["dictpopname"]
self.fhdhr.config.write(config_section, config_name, config_value) elif method == "restart":
restart_thread = threading.Thread(target=self.restart_thread)
restart_thread.start()
return redirect("%s?retmessage=%s" % (redirect_url, urllib.parse.quote("Restarting in %s seconds" % self.restart_sleep)))
elif method == "restart_actual":
session["restart"] = True
if redirect_url: if redirect_url:
return redirect(redirect_url + "?retmessage=" + urllib.parse.quote("%s Success" % method)) return redirect("%s?retmessage=%s" % (redirect_url, urllib.parse.quote("%s Success" % method)))
else: else:
return "%s Success" % method return "%s Success" % method
def restart_thread(self):
time.sleep(self.restart_sleep)
self.fhdhr.api.get(self.restart_url)

View File

@ -0,0 +1,39 @@
class Startup_Tasks():
endpoints = ["/api/startup_tasks"]
endpoint_name = "api_startup_tasks"
endpoint_methods = ["GET", "POST"]
def __init__(self, fhdhr):
self.fhdhr = fhdhr
self.epg_update_url = "/api/epg?method=update"
self.channel_update_url = "/api/channels?method=scan"
def __call__(self, *args):
return self.get(*args)
def get(self, *args):
self.fhdhr.logger.info("Running Startup Tasks.")
# Hit Channel Update API
haseverscanned = self.fhdhr.db.get_fhdhr_value("channels", "scanned_time")
updatechannels = False
if not haseverscanned:
updatechannels = True
elif self.fhdhr.config.dict["fhdhr"]["chanscan_on_start"]:
updatechannels = True
if updatechannels:
for origin in list(self.fhdhr.origins.origins_dict.keys()):
self.fhdhr.api.get("%s&origin=%s" % (self.channel_update_url, origin))
# Hit EPG Update API
for epg_method in self.fhdhr.device.epg.epg_methods:
self.fhdhr.api.get("%s&source=%s" % (self.epg_update_url, epg_method))
self.fhdhr.logger.info("Startup Tasks Complete.")
return "Success"

View File

@ -1,6 +1,6 @@
from flask import Response, request, redirect, abort, stream_with_context from flask import Response, request, redirect, abort, stream_with_context, session
import urllib.parse import urllib.parse
import uuid import json
from fHDHR.exceptions import TunerError from fHDHR.exceptions import TunerError
@ -9,6 +9,9 @@ class Tuners():
endpoints = ["/api/tuners"] endpoints = ["/api/tuners"]
endpoint_name = "api_tuners" endpoint_name = "api_tuners"
endpoint_methods = ["GET", "POST"] endpoint_methods = ["GET", "POST"]
endpoint_default_parameters = {
"method": "status"
}
def __init__(self, fhdhr): def __init__(self, fhdhr):
self.fhdhr = fhdhr self.fhdhr = fhdhr
@ -22,36 +25,66 @@ class Tuners():
accessed_url = request.args.get('accessed', default=request.url, type=str) accessed_url = request.args.get('accessed', default=request.url, type=str)
method = request.args.get('method', default=self.fhdhr.config.dict["fhdhr"]["stream_type"], type=str) method = request.args.get('method', default=self.fhdhr.config.dict["streaming"]["method"], type=str)
tuner_number = request.args.get('tuner', None, type=str) tuner_number = request.args.get('tuner', default=None, type=str)
redirect_url = request.args.get('redirect', default=None, type=str) redirect_url = request.args.get('redirect', default=None, type=str)
if method in ["direct", "ffmpeg", "vlc"]: origin_methods = self.fhdhr.origins.valid_origins
origin = request.args.get('origin', default=None, type=str)
if origin and origin not in origin_methods:
return "%s Invalid channels origin" % origin
if method == "stream":
channel_number = request.args.get('channel', None, type=str) channel_number = request.args.get('channel', None, type=str)
if not channel_number: if not channel_number:
return "Missing Channel" return "Missing Channel"
if str(channel_number) not in [str(x) for x in self.fhdhr.device.channels.get_channel_list("number")]: if origin:
if str(channel_number) in [str(x) for x in self.fhdhr.device.channels.get_channel_list("number", origin)]:
chan_obj = self.fhdhr.device.channels.get_channel_obj("number", channel_number, origin)
elif str(channel_number) in [str(x) for x in self.fhdhr.device.channels.get_channel_list("id", origin)]:
chan_obj = self.fhdhr.device.channels.get_channel_obj("id", channel_number, origin)
else:
response = Response("Not Found", status=404) response = Response("Not Found", status=404)
response.headers["X-fHDHR-Error"] = "801 - Unknown Channel" response.headers["X-fHDHR-Error"] = "801 - Unknown Channel"
self.fhdhr.logger.error(response.headers["X-fHDHR-Error"]) self.fhdhr.logger.error(response.headers["X-fHDHR-Error"])
abort(response) abort(response)
channel_dict = self.fhdhr.device.channels.get_channel_dict("number", channel_number) else:
if not channel_dict["enabled"]:
if str(channel_number) in [str(x) for x in self.fhdhr.device.channels.get_channel_list("id")]:
chan_obj = self.fhdhr.device.channels.get_channel_obj("id", channel_number)
else:
response = Response("Not Found", status=404)
response.headers["X-fHDHR-Error"] = "801 - Unknown Channel"
self.fhdhr.logger.error(response.headers["X-fHDHR-Error"])
abort(response)
if not chan_obj.dict["enabled"]:
response = Response("Service Unavailable", status=503) response = Response("Service Unavailable", status=503)
response.headers["X-fHDHR-Error"] = str("806 - Tune Failed") response.headers["X-fHDHR-Error"] = str("806 - Tune Failed")
self.fhdhr.logger.error(response.headers["X-fHDHR-Error"]) self.fhdhr.logger.error(response.headers["X-fHDHR-Error"])
abort(response) abort(response)
origin = chan_obj.origin
channel_number = chan_obj.number
stream_method = request.args.get('stream_method', default=self.fhdhr.origins.origins_dict[origin].stream_method, type=str)
if stream_method not in list(self.fhdhr.config.dict["streaming"]["valid_methods"].keys()):
response = Response("Service Unavailable", status=503)
response = Response("Service Unavailable", status=503)
response.headers["X-fHDHR-Error"] = str("806 - Tune Failed")
abort(response)
duration = request.args.get('duration', default=0, type=int) duration = request.args.get('duration', default=0, type=int)
transcode = request.args.get('transcode', default=None, type=str) transcode_quality = request.args.get('transcode', default=None, type=str)
valid_transcode_types = [None, "heavy", "mobile", "internet720", "internet480", "internet360", "internet240"] valid_transcode_types = [None, "heavy", "mobile", "internet720", "internet480", "internet360", "internet240"]
if transcode not in valid_transcode_types: if transcode_quality not in valid_transcode_types:
response = Response("Service Unavailable", status=503) response = Response("Service Unavailable", status=503)
response.headers["X-fHDHR-Error"] = "802 - Unknown Transcode Profile" response.headers["X-fHDHR-Error"] = "802 - Unknown Transcode Profile"
self.fhdhr.logger.error(response.headers["X-fHDHR-Error"]) self.fhdhr.logger.error(response.headers["X-fHDHR-Error"])
@ -59,19 +92,21 @@ class Tuners():
stream_args = { stream_args = {
"channel": channel_number, "channel": channel_number,
"method": method, "origin": origin,
"method": stream_method,
"duration": duration, "duration": duration,
"transcode": transcode, "origin_quality": self.fhdhr.config.dict["streaming"]["origin_quality"],
"transcode_quality": transcode_quality or self.fhdhr.config.dict["streaming"]["transcode_quality"],
"accessed": accessed_url, "accessed": accessed_url,
"client": client_address, "client": client_address,
"client_id": str(client_address) + "_" + str(uuid.uuid4()) "client_id": session["session_id"]
} }
try: try:
if not tuner_number: if not tuner_number:
tunernum = self.fhdhr.device.tuners.first_available(channel_number) tunernum = self.fhdhr.device.tuners.first_available(origin, channel_number)
else: else:
tunernum = self.fhdhr.device.tuners.tuner_grab(tuner_number, channel_number) tunernum = self.fhdhr.device.tuners.tuner_grab(tuner_number, origin, channel_number)
except TunerError as e: except TunerError as e:
self.fhdhr.logger.info("A %s stream request for channel %s was rejected due to %s" self.fhdhr.logger.info("A %s stream request for channel %s was rejected due to %s"
% (stream_args["method"], str(stream_args["channel"]), str(e))) % (stream_args["method"], str(stream_args["channel"]), str(e)))
@ -79,47 +114,91 @@ class Tuners():
response.headers["X-fHDHR-Error"] = str(e) response.headers["X-fHDHR-Error"] = str(e)
self.fhdhr.logger.error(response.headers["X-fHDHR-Error"]) self.fhdhr.logger.error(response.headers["X-fHDHR-Error"])
abort(response) abort(response)
tuner = self.fhdhr.device.tuners.tuners[str(tunernum)]
tuner = self.fhdhr.device.tuners.tuners[origin][str(tunernum)]
try: try:
stream_args = self.fhdhr.device.tuners.get_stream_info(stream_args) stream_args = self.fhdhr.device.tuners.get_stream_info(stream_args)
except TunerError as e: except TunerError as e:
self.fhdhr.logger.info("A %s stream request for channel %s was rejected due to %s" self.fhdhr.logger.info("A %s stream request for %s channel %s was rejected due to %s"
% (stream_args["method"], str(stream_args["channel"]), str(e))) % (origin, stream_args["method"], str(stream_args["channel"]), str(e)))
response = Response("Service Unavailable", status=503) response = Response("Service Unavailable", status=503)
response.headers["X-fHDHR-Error"] = str(e) response.headers["X-fHDHR-Error"] = str(e)
self.fhdhr.logger.error(response.headers["X-fHDHR-Error"]) self.fhdhr.logger.error(response.headers["X-fHDHR-Error"])
tuner.close() tuner.close()
abort(response) abort(response)
self.fhdhr.logger.info("Tuner #" + str(tunernum) + " to be used for stream.") self.fhdhr.logger.info("%s Tuner #%s to be used for stream." % (origin, tunernum))
tuner.set_status(stream_args) tuner.set_status(stream_args)
session["tuner_used"] = tunernum
if stream_args["method"] == "direct": try:
return Response(tuner.get_stream(stream_args, tuner), content_type=stream_args["content_type"], direct_passthrough=True) stream = tuner.get_stream(stream_args, tuner)
elif stream_args["method"] in ["ffmpeg", "vlc"]: except TunerError as e:
return Response(stream_with_context(tuner.get_stream(stream_args, tuner)), mimetype=stream_args["content_type"]) response.headers["X-fHDHR-Error"] = str(e)
self.fhdhr.logger.error(response.headers["X-fHDHR-Error"])
tuner.close()
abort(response)
return Response(stream_with_context(stream.get()), mimetype=stream_args["content_type"])
elif method == "close": elif method == "close":
if not tuner_number or str(tuner_number) not in list(self.fhdhr.device.tuners.tuners.keys()): if not origin:
return "Missing Origin"
if not tuner_number or str(tuner_number) not in list(self.fhdhr.device.tuners.tuners[origin].keys()):
return "%s Invalid tuner" % str(tuner_number) return "%s Invalid tuner" % str(tuner_number)
tuner = self.fhdhr.device.tuners.tuners[str(tuner_number)] session["tuner_used"] = tuner_number
tuner = self.fhdhr.device.tuners.tuners[origin][str(tuner_number)]
tuner.close() tuner.close()
elif method == "scan": elif method == "scan":
if not origin:
for origin in list(self.fhdhr.device.tuners.tuners.keys()):
if not tuner_number: if not tuner_number:
self.fhdhr.device.tuners.tuner_scan() tunernum = self.fhdhr.device.tuners.first_available(origin, None)
else: else:
tuner = self.fhdhr.device.tuners.tuners[str(tuner_number)] tunernum = self.fhdhr.device.tuners.tuner_grab(tuner_number, origin, None)
tuner.channel_scan() tuner = self.fhdhr.device.tuners.tuners[origin][str(tunernum)]
tuner.channel_scan(origin=origin, grabbed=False)
else:
if not tuner_number:
tunernum = self.fhdhr.device.tuners.first_available(origin, None)
else:
tunernum = self.fhdhr.device.tuners.tuner_grab(tuner_number, origin, None)
tuner = self.fhdhr.device.tuners.tuners[origin][str(tunernum)]
tuner.channel_scan(origin=origin, grabbed=True)
elif method == "status":
if not origin:
if not tuner_number:
tuner_status = self.fhdhr.device.tuners.status()
else:
tuner_status = ["Invalid Tuner %s" % tuner_number]
else:
if not tuner_number:
tuner_status = self.fhdhr.device.tuners.status(origin)
elif str(tuner_number) in list(self.fhdhr.device.tuners.tuners[origin].keys()):
tuner_status = self.fhdhr.device.tuners.tuners[origin][str(tuner_number)].get_status()
else:
tuner_status = ["Invalid Tuner %s" % tuner_number]
tuner_status_json = json.dumps(tuner_status, indent=4)
return Response(status=200,
response=tuner_status_json,
mimetype='application/json')
else: else:
return "%s Invalid Method" % method return "%s Invalid Method" % method
if redirect_url: if redirect_url:
return redirect(redirect_url + "?retmessage=" + urllib.parse.quote("%s Success" % method)) return redirect("%s?retmessage=%s" % (redirect_url, urllib.parse.quote("%s Success" % method)))
else: else:
return "%s Success" % method return "%s Success" % method

116
fHDHR_web/api/w3u.py Normal file
View File

@ -0,0 +1,116 @@
from flask import Response, request, redirect
import urllib.parse
import json
from fHDHR.tools import channel_sort
class W3U():
endpoints = ["/api/w3u"]
endpoint_name = "api_w3u"
endpoint_methods = ["GET", "POST"]
def __init__(self, fhdhr):
self.fhdhr = fhdhr
def __call__(self, *args):
return self.get(*args)
def get(self, *args):
base_url = request.url_root[:-1]
method = request.args.get('method', default="get", type=str)
channel = request.args.get('channel', default="all", type=str)
redirect_url = request.args.get('redirect', default=None, type=str)
if method == "get":
origin_methods = self.fhdhr.origins.valid_origins
origin = request.args.get('origin', default=None, type=str)
if origin and origin not in origin_methods:
return "%s Invalid channels origin" % origin
channel_info_w3u = {
"name": self.fhdhr.config.dict["fhdhr"]["friendlyname"],
"image": '%s/favicon.ico' % base_url,
"epg": '%s/api/xmltv' % base_url,
"stations": []
}
channel_items = []
if origin:
if channel == "all":
fileName = "channels.w3u"
for fhdhr_id in [x["id"] for x in self.fhdhr.device.channels.get_channels(origin)]:
channel_obj = self.fhdhr.device.channels.get_channel_obj("id", fhdhr_id, origin)
if channel_obj.enabled:
channel_items.append(channel_obj)
elif str(channel) in [str(x) for x in self.fhdhr.device.channels.get_channel_list("number", origin)]:
channel_obj = self.fhdhr.device.channels.get_channel_obj("number", channel, origin)
fileName = "%s.w3u" % channel_obj.number
if channel_obj.enabled:
channel_items.append(channel_obj)
else:
return "Channel Disabled"
elif channel != "all" and str(channel) in [str(x) for x in self.fhdhr.device.channels.get_channel_list("id", origin)]:
channel_obj = self.fhdhr.device.channels.get_channel_obj("id", channel, origin)
fileName = "%s.w3u" % channel_obj.number
if channel_obj.enabled:
channel_items.append(channel_obj)
else:
return "Channel Disabled"
elif not origin and channel == "all":
fileName = "channels.w3u"
for origin in list(self.fhdhr.origins.origins_dict.keys()):
for fhdhr_id in [x["id"] for x in self.fhdhr.device.channels.get_channels(origin)]:
channel_obj = self.fhdhr.device.channels.get_channel_obj("id", fhdhr_id, origin)
if channel_obj.enabled:
channel_items.append(channel_obj)
elif not origin and channel != "all" and str(channel) in [str(x) for x in self.fhdhr.device.channels.get_channel_list("id")]:
channel_obj = self.fhdhr.device.channels.get_channel_obj("id", channel)
fileName = "%s.w3u" % channel_obj.number
if channel_obj.enabled:
channel_items.append(channel_obj)
else:
return "Channel Disabled"
else:
return "Invalid Channel"
channels_info = {}
for channel_obj in channel_items:
if self.fhdhr.config.dict["epg"]["images"] == "proxy" or not channel_obj.thumbnail:
logourl = ('%s/api/images?method=get&type=channel&id=%s' %
(base_url, str(channel_obj.dict['origin_id'])))
else:
logourl = channel_obj.thumbnail
channels_info[channel_obj.number] = {
"name": str(channel_obj.dict['name']),
"url": "%s%s" % (base_url, channel_obj.api_stream_url),
"epgId": str(channel_obj.dict['origin_id']),
"image": logourl,
}
# Sort the channels
sorted_channel_list = channel_sort(list(channels_info.keys()))
for channel in sorted_channel_list:
channel_info_w3u["stations"].append(channels_info[channel])
channels_info_json = json.dumps(channel_info_w3u, indent=4)
resp = Response(status=200, response=channels_info_json, mimetype='application/json')
resp.headers["content-disposition"] = "attachment; filename=%s" % fileName
return resp
return Response(status=200,
response=channels_info_json,
mimetype='application/json')
if redirect_url:
return redirect("%s?retmessage=%s" % (redirect_url, urllib.parse.quote("%s Success" % method)))
else:
return "%s Success" % method

View File

@ -1,7 +1,8 @@
from flask import Response, request, redirect from flask import Response, request, redirect, session
import xml.etree.ElementTree import xml.etree.ElementTree
from io import BytesIO from io import BytesIO
import urllib.parse import urllib.parse
import datetime
from fHDHR.tools import sub_el from fHDHR.tools import sub_el
@ -15,14 +16,21 @@ class xmlTV():
def __init__(self, fhdhr): def __init__(self, fhdhr):
self.fhdhr = fhdhr self.fhdhr = fhdhr
self.xmltv_offset = {}
for epg_method in list(self.fhdhr.device.epg.epg_handling.keys()):
if epg_method in list(self.fhdhr.config.dict.keys()):
if "xmltv_offset" in list(self.fhdhr.config.dict[epg_method].keys()):
self.xmltv_offset[epg_method] = self.fhdhr.config.dict[epg_method]["xmltv_offset"]
if epg_method not in list(self.xmltv_offset.keys()):
self.xmltv_offset[epg_method] = self.fhdhr.config.dict["epg"]["xmltv_offset"]
def __call__(self, *args): def __call__(self, *args):
return self.get(*args) return self.get(*args)
def get(self, *args): def get(self, *args):
if self.fhdhr.config.dict["fhdhr"]["require_auth"]: if self.fhdhr.config.dict["fhdhr"]["require_auth"]:
DeviceAuth = request.args.get('DeviceAuth', default=None, type=str) if session["deviceauth"] != self.fhdhr.config.dict["fhdhr"]["device_auth"]:
if DeviceAuth != self.fhdhr.config.dict["fhdhr"]["device_auth"]:
return "not subscribed" return "not subscribed"
base_url = request.url_root[:-1] base_url = request.url_root[:-1]
@ -30,7 +38,7 @@ class xmlTV():
method = request.args.get('method', default="get", type=str) method = request.args.get('method', default="get", type=str)
source = request.args.get('source', default=self.fhdhr.config.dict["epg"]["def_method"], type=str) source = request.args.get('source', default=self.fhdhr.config.dict["epg"]["def_method"], type=str)
if source not in self.fhdhr.config.dict["main"]["valid_epg_methods"]: if source not in list(self.fhdhr.config.dict["epg"]["valid_methods"].keys()):
return "%s Invalid xmltv method" % source return "%s Invalid xmltv method" % source
redirect_url = request.args.get('redirect', default=None, type=str) redirect_url = request.args.get('redirect', default=None, type=str)
@ -38,6 +46,18 @@ class xmlTV():
if method == "get": if method == "get":
epgdict = self.fhdhr.device.epg.get_epg(source) epgdict = self.fhdhr.device.epg.get_epg(source)
if source in self.fhdhr.origins.valid_origins:
epgdict = epgdict.copy()
for c in list(epgdict.keys()):
chan_obj = self.fhdhr.device.channels.get_channel_obj("origin_id", epgdict[c]["id"], source)
epgdict[chan_obj.number] = epgdict.pop(c)
epgdict[chan_obj.number]["name"] = chan_obj.dict["name"]
epgdict[chan_obj.number]["callsign"] = chan_obj.dict["callsign"]
epgdict[chan_obj.number]["number"] = chan_obj.number
epgdict[chan_obj.number]["id"] = chan_obj.dict["origin_id"]
epgdict[chan_obj.number]["thumbnail"] = chan_obj.thumbnail
xmltv_xml = self.create_xmltv(base_url, epgdict, source) xmltv_xml = self.create_xmltv(base_url, epgdict, source)
return Response(status=200, return Response(status=200,
@ -54,7 +74,7 @@ class xmlTV():
return "%s Invalid Method" % method return "%s Invalid Method" % method
if redirect_url: if redirect_url:
return redirect(redirect_url + "?retmessage=" + urllib.parse.quote("%s Success" % method)) return redirect("%s?retmessage=%s" % (redirect_url, urllib.parse.quote("%s Success" % method)))
else: else:
return "%s Success" % method return "%s Success" % method
@ -64,7 +84,7 @@ class xmlTV():
xmltvgen.set('source-info-url', self.fhdhr.config.dict["fhdhr"]["friendlyname"]) xmltvgen.set('source-info-url', self.fhdhr.config.dict["fhdhr"]["friendlyname"])
xmltvgen.set('source-info-name', self.fhdhr.config.dict["main"]["servicename"]) xmltvgen.set('source-info-name', self.fhdhr.config.dict["main"]["servicename"])
xmltvgen.set('generator-info-name', 'fHDHR') xmltvgen.set('generator-info-name', 'fHDHR')
xmltvgen.set('generator-info-url', 'fHDHR/' + self.fhdhr.config.dict["main"]["reponame"]) xmltvgen.set('generator-info-url', 'fHDHR/%s' % self.fhdhr.config.dict["main"]["reponame"])
return xmltvgen return xmltvgen
def xmltv_file(self, xmltvgen): def xmltv_file(self, xmltvgen):
@ -78,6 +98,14 @@ class xmlTV():
"""This method is called when creation of a full xmltv is not possible""" """This method is called when creation of a full xmltv is not possible"""
return self.xmltv_file(self.xmltv_headers()) return self.xmltv_file(self.xmltv_headers())
def timestamp_to_datetime(self, time_start, time_end, source):
xmltvtimetamps = {}
source_offset = self.xmltv_offset[source]
for time_item, time_value in zip(["time_start", "time_end"], [time_start, time_end]):
timestampval = datetime.datetime.fromtimestamp(time_value).strftime('%Y%m%d%H%M%S')
xmltvtimetamps[time_item] = "%s %s" % (timestampval, source_offset)
return xmltvtimetamps
def create_xmltv(self, base_url, epgdict, source): def create_xmltv(self, base_url, epgdict, source):
if not epgdict: if not epgdict:
return self.xmltv_empty() return self.xmltv_empty()
@ -85,15 +113,15 @@ class xmlTV():
out = self.xmltv_headers() out = self.xmltv_headers()
if source in ["origin", "blocks", self.fhdhr.config.dict["main"]["dictpopname"]]: if source in self.fhdhr.origins.valid_origins:
for c in list(epgdict.keys()): for c in list(epgdict.keys()):
chan_obj = self.fhdhr.device.channels.get_channel_obj("origin_id", epgdict[c]["id"]) chan_obj = self.fhdhr.device.channels.get_channel_obj("origin_id", epgdict[c]["id"], source)
epgdict[chan_obj.dict["number"]] = epgdict.pop(c) epgdict[chan_obj.number] = epgdict.pop(c)
epgdict[chan_obj.dict["number"]]["name"] = chan_obj.dict["name"] epgdict[chan_obj.number]["name"] = chan_obj.dict["name"]
epgdict[chan_obj.dict["number"]]["callsign"] = chan_obj.dict["callsign"] epgdict[chan_obj.number]["callsign"] = chan_obj.dict["callsign"]
epgdict[chan_obj.dict["number"]]["number"] = chan_obj.dict["number"] epgdict[chan_obj.number]["number"] = chan_obj.number
epgdict[chan_obj.dict["number"]]["id"] = chan_obj.dict["origin_id"] epgdict[chan_obj.number]["id"] = chan_obj.dict["origin_id"]
epgdict[chan_obj.dict["number"]]["thumbnail"] = chan_obj.thumbnail epgdict[chan_obj.number]["thumbnail"] = chan_obj.thumbnail
for c in list(epgdict.keys()): for c in list(epgdict.keys()):
@ -107,7 +135,7 @@ class xmlTV():
sub_el(c_out, 'display-name', text=epgdict[c]['name']) sub_el(c_out, 'display-name', text=epgdict[c]['name'])
if self.fhdhr.config.dict["epg"]["images"] == "proxy": if self.fhdhr.config.dict["epg"]["images"] == "proxy":
sub_el(c_out, 'icon', src=(str(base_url) + "/api/images?method=get&type=channel&id=" + str(epgdict[c]['id']))) sub_el(c_out, 'icon', src=("%s/api/images?method=get&type=channel&id=%s" % (base_url, epgdict[c]['id'])))
else: else:
sub_el(c_out, 'icon', src=(epgdict[c]["thumbnail"])) sub_el(c_out, 'icon', src=(epgdict[c]["thumbnail"]))
@ -117,16 +145,18 @@ class xmlTV():
for program in channel_listing: for program in channel_listing:
xmltvtimetamps = self.timestamp_to_datetime(program['time_start'], program['time_end'], source)
prog_out = sub_el(out, 'programme', prog_out = sub_el(out, 'programme',
start=program['time_start'], start=xmltvtimetamps['time_start'],
stop=program['time_end'], stop=xmltvtimetamps['time_end'],
channel=str(channelnum)) channel=str(channelnum))
sub_el(prog_out, 'title', lang='en', text=program['title']) sub_el(prog_out, 'title', lang='en', text=program['title'])
sub_el(prog_out, 'desc', lang='en', text=program['description']) sub_el(prog_out, 'desc', lang='en', text=program['description'])
sub_el(prog_out, 'sub-title', lang='en', text='Movie: ' + program['sub-title']) sub_el(prog_out, 'sub-title', lang='en', text='Movie: %s' % program['sub-title'])
sub_el(prog_out, 'length', units='minutes', text=str(int(program['duration_minutes']))) sub_el(prog_out, 'length', units='minutes', text=str(int(program['duration_minutes'])))
@ -148,11 +178,11 @@ class xmlTV():
if program["thumbnail"]: if program["thumbnail"]:
if self.fhdhr.config.dict["epg"]["images"] == "proxy": if self.fhdhr.config.dict["epg"]["images"] == "proxy":
sub_el(prog_out, 'icon', src=(str(base_url) + "/api/images?method=get&type=content&id=" + str(program['id']))) sub_el(prog_out, 'icon', src=("%s/api/images?method=get&type=content&id=%s" % (base_url, program['id'])))
else: else:
sub_el(prog_out, 'icon', src=(program["thumbnail"])) sub_el(prog_out, 'icon', src=(program["thumbnail"]))
else: else:
sub_el(prog_out, 'icon', src=(str(base_url) + "/api/images?method=generate&type=content&message=" + urllib.parse.quote(program['title']))) sub_el(prog_out, 'icon', src=("%s/api/images?method=generate&type=content&message=%s" % (base_url, urllib.parse.quote(program['title']))))
if program['rating']: if program['rating']:
rating_out = sub_el(prog_out, 'rating', system="MPAA") rating_out = sub_el(prog_out, 'rating', system="MPAA")

View File

@ -0,0 +1,17 @@
from .brython import Brython
from .brython_stdlib import Brython_stdlib
from .brython_bry import Brython_bry
class fHDHR_Brython():
def __init__(self, fhdhr):
self.fhdhr = fhdhr
self.brython = Brython(fhdhr)
self.brython_stdlib = Brython_stdlib(fhdhr)
self.brython_bry = Brython_bry(fhdhr)

14160
fHDHR_web/brython/brython.js Normal file

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,21 @@
from flask import send_from_directory
import pathlib
class Brython():
endpoints = ["/brython.js"]
endpoint_name = "file_brython_js"
def __init__(self, fhdhr):
self.fhdhr = fhdhr
self.brython_path = pathlib.Path(self.fhdhr.config.internal["paths"]["fHDHR_web_dir"]).joinpath('brython')
def __call__(self, *args):
return self.get(*args)
def get(self, *args):
return send_from_directory(self.brython_path,
'brython.js',
mimetype='text/javascript')

View File

@ -0,0 +1,19 @@
from flask import send_from_directory
import pathlib
class Brython_bry():
endpoints = ["/brython.bry"]
endpoint_name = "file_brython_bry"
def __init__(self, fhdhr):
self.fhdhr = fhdhr
self.brython_path = pathlib.Path(self.fhdhr.config.internal["paths"]["fHDHR_web_dir"]).joinpath('brython')
def __call__(self, *args):
return self.get(*args)
def get(self, *args):
return send_from_directory(self.brython_path, 'brython_code.py')

View File

@ -0,0 +1,104 @@
from browser import document, bind # alert, window
import json
def chan_edit_data(items, channel_id):
chanlist = []
chandict = {}
for element in items:
if element.name == "id":
if len(chandict.keys()) >= 2 and "id" in list(chandict.keys()):
chanlist.append(chandict)
chandict = {"id": element.value}
if element.type == "checkbox":
if element.name in ["enabled"]:
save_val = element.checked
else:
save_val = int(element.checked)
else:
save_val = element.value
if element.name != "id":
cur_value = element.placeholder
if element.type == "checkbox":
if element.name in ["enabled"]:
cur_value = element.placeholder
else:
cur_value = int(element.placeholder)
if str(save_val) != str(cur_value):
chandict[element.name] = save_val
if channel_id != "all":
chanlist == [x for x in chanlist if x["id"] == channel_id]
return chanlist
def chan_edit_postform(chanlist):
origin = document["origin"].value
postForm = document.createElement('form')
postForm.method = "POST"
postForm.action = "/api/channels?method=modify&origin=%s&redirect=/channels_editor&origin=%s" % (origin, origin)
postForm.setRequestHeader = "('Content-Type', 'application/json')"
postData = document.createElement('input')
postData.type = 'hidden'
postData.name = "channels"
postData.value = json.dumps(chanlist)
postForm.appendChild(postData)
document.body.appendChild(postForm)
return postForm
@bind("#Chan_Edit_Reset", "submit")
def chan_edit_reset(evt):
chanlist = chan_edit_data(
document.select(".reset"),
str(evt.currentTarget.children[0].id).replace("reset_", ""))
postForm = chan_edit_postform(chanlist)
postForm.submit()
evt.preventDefault()
@bind("#Chan_Edit_Modify", "submit")
def chan_edit_modify(evt):
chanlist = chan_edit_data(
document.select(".channels"),
str(evt.currentTarget.children[0].id).replace("update_", ""))
postForm = chan_edit_postform(chanlist)
postForm.submit()
evt.preventDefault()
@bind("#Chan_Edit_Enable_Toggle", "click")
def chan_edit_enable(event):
enable_bool = bool(int(document["enable_button"].value))
for element in document.get(selector='input[type="checkbox"]'):
if element.name == "enabled":
element.checked = enable_bool
element.value = enable_bool
if not enable_bool:
document["enable_button"].value = "1"
document["enable_button"].text = "Enable All"
else:
document["enable_button"].value = "0"
document["enable_button"].text = "Disable All"
@bind("#Chan_Edit_Favorite_Toggle", "click")
def chan_edit_favorite(event):
enable_bool = bool(int(document["favorite_button"].value))
for element in document.get(selector='input[type="checkbox"]'):
if element.name == "favorite":
element.checked = enable_bool
element.value = int(enable_bool)
if not enable_bool:
document["favorite_button"].value = "1"
document["favorite_button"].text = "Favorite All"
else:
document["favorite_button"].value = "0"
document["favorite_button"].text = "Unfavorite All"

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,22 @@
from flask import send_from_directory
import pathlib
class Brython_stdlib():
endpoints = ["/brython_stdlib.js"]
endpoint_name = "file_brython_stdlib_js"
def __init__(self, fhdhr):
self.fhdhr = fhdhr
self.brython_path = pathlib.Path(self.fhdhr.config.internal["paths"]["fHDHR_web_dir"]).joinpath('brython')
def __call__(self, *args):
return self.get(*args)
def get(self, *args):
return send_from_directory(self.brython_path,
'brython_stdlib.js',
mimetype='text/javascript')

View File

@ -2,7 +2,6 @@
from .favicon_ico import Favicon_ICO from .favicon_ico import Favicon_ICO
from .style_css import Style_CSS from .style_css import Style_CSS
from .device_xml import Device_XML
class fHDHR_Files(): class fHDHR_Files():
@ -12,4 +11,3 @@ class fHDHR_Files():
self.favicon = Favicon_ICO(fhdhr) self.favicon = Favicon_ICO(fhdhr)
self.style = Style_CSS(fhdhr) self.style = Style_CSS(fhdhr)
self.device_xml = Device_XML(fhdhr)

View File

@ -1,21 +0,0 @@
from flask import request, redirect
class Device_XML():
endpoints = ["/device.xml"]
endpoint_name = "file_device_xml"
def __init__(self, fhdhr):
self.fhdhr = fhdhr
def __call__(self, *args):
return self.get(*args)
def get(self, *args):
user_agent = request.headers.get('User-Agent')
if (self.fhdhr.config.dict["rmg"]["enabled"] and
str(user_agent).lower().startswith("plexmediaserver")):
return redirect("/rmg/device.xml")
else:
return redirect("/hdhr/device.xml")

View File

@ -1,37 +0,0 @@
from flask import Response, request
import json
class Lineup_JSON():
endpoints = ["/lineup.json", "/hdhr/lineup.json"]
endpoint_name = "hdhr_lineup_json"
def __init__(self, fhdhr):
self.fhdhr = fhdhr
def __call__(self, *args):
return self.get(*args)
def get(self, *args):
base_url = request.url_root[:-1]
show = request.args.get('show', default="all", type=str)
jsonlineup = []
for fhdhr_id in list(self.fhdhr.device.channels.list.keys()):
channel_obj = self.fhdhr.device.channels.list[fhdhr_id]
if channel_obj.enabled or show == "found":
lineup_dict = channel_obj.lineup_dict
lineup_dict["URL"] = "%s%s" % (base_url, lineup_dict["URL"])
if show == "found" and channel_obj.enabled:
lineup_dict["Enabled"] = 1
elif show == "found" and not channel_obj.enabled:
lineup_dict["Enabled"] = 0
jsonlineup.append(lineup_dict)
lineup_json = json.dumps(jsonlineup, indent=4)
return Response(status=200,
response=lineup_json,
mimetype='application/json')

View File

@ -1,45 +0,0 @@
from flask import Response, request
from io import BytesIO
import xml.etree.ElementTree
from fHDHR.tools import sub_el
class Lineup_XML():
endpoints = ["/lineup.xml", "/hdhr/lineup.xml"]
endpoint_name = "hdhr_lineup_xml"
def __init__(self, fhdhr):
self.fhdhr = fhdhr
def __call__(self, *args):
return self.get(*args)
def get(self, *args):
base_url = request.url_root[:-1]
show = request.args.get('show', default="all", type=str)
out = xml.etree.ElementTree.Element('Lineup')
for fhdhr_id in list(self.fhdhr.device.channels.list.keys()):
channel_obj = self.fhdhr.device.channels.list[fhdhr_id]
if channel_obj.enabled or show == "found":
program_out = sub_el(out, 'Program')
lineup_dict = channel_obj.lineup_dict
lineup_dict["URL"] = base_url + lineup_dict["URL"]
if show == "found" and channel_obj.enabled:
lineup_dict["Enabled"] = 1
elif show == "found" and not channel_obj.enabled:
lineup_dict["Enabled"] = 0
for key in list(lineup_dict.keys()):
sub_el(program_out, str(key), str(lineup_dict[key]))
fakefile = BytesIO()
fakefile.write(b'<?xml version="1.0" encoding="UTF-8"?>\n')
fakefile.write(xml.etree.ElementTree.tostring(out, encoding='UTF-8'))
lineup_xml = fakefile.getvalue()
return Response(status=200,
response=lineup_xml,
mimetype='application/xml')

View File

@ -1,16 +1,14 @@
from .index_html import Index_HTML from .index_html import Index_HTML
from .origin_html import Origin_HTML
from .channels_html import Channels_HTML from .channels_html import Channels_HTML
from .guide_html import Guide_HTML from .guide_html import Guide_HTML
from .cluster_html import Cluster_HTML
from .tuners_html import Tuners_HTML from .tuners_html import Tuners_HTML
from .xmltv_html import xmlTV_HTML from .xmltv_html import xmlTV_HTML
from .version_html import Version_HTML from .version_html import Version_HTML
from .diagnostics_html import Diagnostics_HTML from .diagnostics_html import Diagnostics_HTML
from .settings_html import Settings_HTML from .settings_html import Settings_HTML
from .channels_editor import Channels_Editor_HTML from .channels_editor_html import Channels_Editor_HTML
class fHDHR_Pages(): class fHDHR_Pages():
@ -19,11 +17,9 @@ class fHDHR_Pages():
self.fhdhr = fhdhr self.fhdhr = fhdhr
self.index_html = Index_HTML(fhdhr) self.index_html = Index_HTML(fhdhr)
self.origin_html = Origin_HTML(fhdhr)
self.channels_html = Channels_HTML(fhdhr) self.channels_html = Channels_HTML(fhdhr)
self.channels_editor = Channels_Editor_HTML(fhdhr) self.channels_editor_html = Channels_Editor_HTML(fhdhr)
self.guide_html = Guide_HTML(fhdhr) self.guide_html = Guide_HTML(fhdhr)
self.cluster_html = Cluster_HTML(fhdhr)
self.tuners_html = Tuners_HTML(fhdhr) self.tuners_html = Tuners_HTML(fhdhr)
self.xmltv_html = xmlTV_HTML(fhdhr) self.xmltv_html = xmlTV_HTML(fhdhr)
self.version_html = Version_HTML(fhdhr) self.version_html = Version_HTML(fhdhr)

View File

@ -1,23 +0,0 @@
from flask import request, render_template
class Channels_Editor_HTML():
endpoints = ["/channels_editor", "/channels_editor.html"]
endpoint_name = "page_channels_editor_html"
def __init__(self, fhdhr):
self.fhdhr = fhdhr
def __call__(self, *args):
return self.get(*args)
def get(self, *args):
channelslist = []
for fhdhr_id in list(self.fhdhr.device.channels.list.keys()):
channel_obj = self.fhdhr.device.channels.list[fhdhr_id]
channel_dict = channel_obj.dict.copy()
channel_dict["play_url"] = channel_obj.play_url
channelslist.append(channel_dict)
return render_template('channels_editor.html', request=request, fhdhr=self.fhdhr, channelslist=channelslist)

View File

@ -0,0 +1,43 @@
from flask import request, render_template, session
from fHDHR.tools import channel_sort
class Channels_Editor_HTML():
endpoints = ["/channels_editor", "/channels_editor.html"]
endpoint_name = "page_channels_editor_html"
endpoint_access_level = 2
endpoint_category = "tool_pages"
pretty_name = "Channels Editor"
def __init__(self, fhdhr):
self.fhdhr = fhdhr
def __call__(self, *args):
return self.get(*args)
def get(self, *args):
origin = request.args.get('origin', default=self.fhdhr.device.epg.def_method, type=str)
origin_methods = self.fhdhr.origins.valid_origins
if origin not in origin_methods:
origin = origin_methods[0]
channelslist = {}
for fhdhr_id in [x["id"] for x in self.fhdhr.device.channels.get_channels(origin)]:
channel_obj = self.fhdhr.device.channels.get_channel_obj("id", fhdhr_id, origin)
channel_dict = channel_obj.dict.copy()
channel_dict["number"] = channel_obj.number
channel_dict["chan_thumbnail"] = channel_obj.thumbnail
channel_dict["m3u_url"] = channel_obj.api_m3u_url
channelslist[channel_dict["number"]] = channel_dict
# Sort the channels
sorted_channel_list = channel_sort(list(channelslist.keys()))
sorted_chan_guide = []
for channel in sorted_channel_list:
sorted_chan_guide.append(channelslist[channel])
return render_template('channels_editor.html', request=request, session=session, fhdhr=self.fhdhr, channelslist=sorted_chan_guide, origin=origin, origin_methods=origin_methods, list=list)

View File

@ -1,9 +1,13 @@
from flask import request, render_template from flask import request, render_template, session
from fHDHR.tools import channel_sort
class Channels_HTML(): class Channels_HTML():
endpoints = ["/channels", "/channels.html"] endpoints = ["/channels", "/channels.html"]
endpoint_name = "page_channels_html" endpoint_name = "page_channels_html"
endpoint_access_level = 0
pretty_name = "Channels"
def __init__(self, fhdhr): def __init__(self, fhdhr):
self.fhdhr = fhdhr self.fhdhr = fhdhr
@ -13,18 +17,33 @@ class Channels_HTML():
def get(self, *args): def get(self, *args):
origin = request.args.get('origin', default=self.fhdhr.device.epg.def_method, type=str)
origin_methods = self.fhdhr.origins.valid_origins
if origin not in origin_methods:
origin = origin_methods[0]
channels_dict = { channels_dict = {
"Total Channels": len(list(self.fhdhr.device.channels.list.keys())), "Total Channels": len(self.fhdhr.device.channels.get_channels(origin)),
"Enabled": 0, "Enabled": 0
} }
channelslist = [] channelslist = {}
for fhdhr_id in list(self.fhdhr.device.channels.list.keys()): for fhdhr_id in [x["id"] for x in self.fhdhr.device.channels.get_channels(origin)]:
channel_obj = self.fhdhr.device.channels.list[fhdhr_id] channel_obj = self.fhdhr.device.channels.get_channel_obj("id", fhdhr_id, origin)
channel_dict = channel_obj.dict.copy() channel_dict = channel_obj.dict.copy()
channel_dict["play_url"] = channel_obj.play_url
channelslist.append(channel_dict) channel_dict["number"] = channel_obj.number
channel_dict["chan_thumbnail"] = channel_obj.thumbnail
channel_dict["m3u_url"] = channel_obj.api_m3u_url
channelslist[channel_dict["number"]] = channel_dict
if channel_dict["enabled"]: if channel_dict["enabled"]:
channels_dict["Enabled"] += 1 channels_dict["Enabled"] += 1
return render_template('channels.html', request=request, fhdhr=self.fhdhr, channelslist=channelslist, channels_dict=channels_dict, list=list) # Sort the channels
sorted_channel_list = channel_sort(list(channelslist.keys()))
sorted_chan_guide = []
for channel in sorted_channel_list:
sorted_chan_guide.append(channelslist[channel])
return render_template('channels.html', request=request, session=session, fhdhr=self.fhdhr, channelslist=sorted_chan_guide, channels_dict=channels_dict, origin=origin, origin_methods=origin_methods, list=list)

View File

@ -1,50 +0,0 @@
from flask import request, render_template
import urllib.parse
class Cluster_HTML():
endpoints = ["/cluster", "/cluster.html"]
endpoint_name = "page_cluster_html"
def __init__(self, fhdhr):
self.fhdhr = fhdhr
self.location_dict = {
"name": self.fhdhr.config.dict["fhdhr"]["friendlyname"],
"location": self.fhdhr.api.base,
"joined": "N/A",
"url_query": self.fhdhr.api.base_quoted
}
def __call__(self, *args):
return self.get(*args)
def get(self, *args):
locations_list = []
if self.fhdhr.config.dict["fhdhr"]["discovery_address"]:
locations_list.append(self.location_dict)
fhdhr_list = self.fhdhr.device.cluster.get_list()
for location in list(fhdhr_list.keys()):
if location in list(self.fhdhr.device.cluster.cluster().keys()):
location_name = self.fhdhr.device.cluster.cluster()[location]["name"]
else:
try:
location_info_url = location + "/discover.json"
location_info_req = self.fhdhr.web.session.get(location_info_url)
location_info = location_info_req.json()
location_name = location_info["FriendlyName"]
except self.fhdhr.web.exceptions.ConnectionError:
self.fhdhr.logger.error("Unreachable: " + location)
location_dict = {
"name": location_name,
"location": location,
"joined": str(fhdhr_list[location]["Joined"]),
"url_query": urllib.parse.quote(location)
}
locations_list.append(location_dict)
return render_template('cluster.html', request=request, fhdhr=self.fhdhr, locations_list=locations_list)

View File

@ -1,9 +1,12 @@
from flask import request, render_template from flask import request, render_template, session
class Diagnostics_HTML(): class Diagnostics_HTML():
endpoints = ["/diagnostics", "/diagnostics.html"] endpoints = ["/diagnostics", "/diagnostics.html"]
endpoint_name = "page_diagnostics_html" endpoint_name = "page_diagnostics_html"
endpoint_access_level = 2
endpoint_category = "tool_pages"
pretty_name = "Diagnostics"
def __init__(self, fhdhr): def __init__(self, fhdhr):
self.fhdhr = fhdhr self.fhdhr = fhdhr
@ -15,111 +18,33 @@ class Diagnostics_HTML():
base_url = request.url_root[:-1] base_url = request.url_root[:-1]
button_list = [] button_dict = {}
button_list.append({ for route_group in list(session["route_list"].keys()):
"label": "Debug Json", if route_group not in ["pages", "brython", "files", "tool_pages"]:
"hdhr": None, button_dict[route_group] = []
"rmg": None, for route_item in list(session["route_list"][route_group].keys()):
"other": "/api/debug", if not session["route_list"][route_group][route_item]["name"].startswith("page_"):
}) button_link = session["route_list"][route_group][route_item]["endpoints"][0]
parameter_index = 0
for parameter in list(session["route_list"][route_group][route_item]["endpoint_default_parameters"].keys()):
parameter_val = session["route_list"][route_group][route_item]["endpoint_default_parameters"][parameter]
if not parameter_index:
button_link += "?"
else:
button_link += "&"
button_link += "%s=%s" % (parameter, parameter_val)
button_link = button_link.replace("<devicekey>", self.fhdhr.config.dict["main"]["uuid"])
button_link = button_link.replace("<base_url>", base_url)
curr_button_dict = {
"label": session["route_list"][route_group][route_item]["pretty_name"],
"link": button_link,
"methods": ",".join(session["route_list"][route_group][route_item]["endpoint_methods"]),
"button": True
}
if ("GET" not in session["route_list"][route_group][route_item]["endpoint_methods"]
or "<tuner_number>" in button_link or "<channel>" in button_link):
curr_button_dict["button"] = False
button_dict[route_group].append(curr_button_dict)
button_list.append({ return render_template('diagnostics.html', request=request, session=session, fhdhr=self.fhdhr, button_dict=button_dict, list=list)
"label": "Cluster Json",
"hdhr": None,
"rmg": None,
"other": "/api/cluster?method=get",
})
button_list.append({
"label": "Lineup XML",
"hdhr": "/lineup.xml",
"rmg": None,
"other": None,
})
button_list.append({
"label": "Lineup JSON",
"hdhr": "/hdhr/lineup.json",
"rmg": None,
"other": None,
})
button_list.append({
"label": "Lineup Status",
"hdhr": "/hdhr/lineup_status.json",
"rmg": None,
"other": None,
})
button_list.append({
"label": "Discover Json",
"hdhr": "/hdhr/discover.json",
"rmg": None,
"other": None,
})
button_list.append({
"label": "Device XML",
"hdhr": "/hdhr/device.xml",
"rmg": "/rmg/device.xml",
"other": None,
})
button_list.append({
"label": "RMG Identification XML",
"hdhr": "",
"rmg": "/rmg",
"other": None,
})
button_list.append({
"label": "RMG Devices Discover",
"hdhr": "",
"rmg": "/rmg/devices/discover",
"other": None,
})
button_list.append({
"label": "RMG Devices Probe",
"hdhr": "",
"rmg": "/rmg/devices/probe?uri=%s" % base_url,
"other": None,
})
button_list.append({
"label": "RMG Devices by DeviceKey",
"hdhr": "",
"rmg": "/rmg/devices/%s" % self.fhdhr.config.dict["main"]["uuid"],
"other": None,
})
button_list.append({
"label": "RMG Channels by DeviceKey",
"hdhr": "",
"rmg": "/rmg/devices/%s/channels" % self.fhdhr.config.dict["main"]["uuid"],
"other": None,
})
button_list.append({
"label": "RMG Scanners by DeviceKey",
"hdhr": "",
"rmg": "/rmg/devices/%s/scanners" % self.fhdhr.config.dict["main"]["uuid"],
"other": None,
})
button_list.append({
"label": "RMG Networks by DeviceKey",
"hdhr": "",
"rmg": "/rmg/devices/%s/networks" % self.fhdhr.config.dict["main"]["uuid"],
"other": None,
})
button_list.append({
"label": "RMG Scan by DeviceKey",
"hdhr": "",
"rmg": "/rmg/devices/%s/scan" % self.fhdhr.config.dict["main"]["uuid"],
"other": None,
})
return render_template('diagnostics.html', request=request, fhdhr=self.fhdhr, button_list=button_list)

View File

@ -1,12 +1,14 @@
from flask import request, render_template from flask import request, render_template, session
import datetime import datetime
from fHDHR.tools import humanized_time from fHDHR.tools import humanized_time, channel_sort
class Guide_HTML(): class Guide_HTML():
endpoints = ["/guide", "/guide.html"] endpoints = ["/guide", "/guide.html"]
endpoint_name = "page_guide_html" endpoint_name = "page_guide_html"
endpoint_access_level = 0
pretty_name = "Guide"
def __init__(self, fhdhr): def __init__(self, fhdhr):
self.fhdhr = fhdhr self.fhdhr = fhdhr
@ -16,7 +18,7 @@ class Guide_HTML():
def get(self, *args): def get(self, *args):
nowtime = datetime.datetime.utcnow() nowtime = datetime.datetime.utcnow().timestamp()
chan_guide_list = [] chan_guide_list = []
@ -25,19 +27,58 @@ class Guide_HTML():
if source not in epg_methods: if source not in epg_methods:
source = self.fhdhr.device.epg.def_method source = self.fhdhr.device.epg.def_method
for channel in self.fhdhr.device.epg.whats_on_allchans(source): if not source:
end_time = datetime.datetime.strptime(channel["listing"][0]["time_end"], '%Y%m%d%H%M%S +0000') return render_template('guide.html', request=request, session=session, fhdhr=self.fhdhr, chan_guide_list=chan_guide_list, epg_methods=epg_methods, source=source, list=list)
remaining_time = humanized_time(int((end_time - nowtime).total_seconds()))
whatson = self.fhdhr.device.epg.whats_on_allchans(source)
# Sort the channels
sorted_channel_list = channel_sort(list(whatson.keys()))
sorted_chan_guide = {}
for channel in sorted_channel_list:
sorted_chan_guide[channel] = whatson[channel]
for channel in list(sorted_chan_guide.keys()):
if sorted_chan_guide[channel]["listing"][0]["time_end"]:
remaining_time = humanized_time(sorted_chan_guide[channel]["listing"][0]["time_end"] - nowtime)
else:
remaining_time = "N/A"
chan_dict = { chan_dict = {
"name": channel["name"], "name": sorted_chan_guide[channel]["name"],
"number": channel["number"], "number": sorted_chan_guide[channel]["number"],
"chan_thumbnail": channel["thumbnail"], "chan_thumbnail": sorted_chan_guide[channel]["thumbnail"],
"listing_title": channel["listing"][0]["title"], "listing_title": sorted_chan_guide[channel]["listing"][0]["title"],
"listing_thumbnail": channel["listing"][0]["thumbnail"], "listing_thumbnail": sorted_chan_guide[channel]["listing"][0]["thumbnail"],
"listing_description": channel["listing"][0]["description"], "listing_description": sorted_chan_guide[channel]["listing"][0]["description"],
"remaining_time": str(remaining_time) "listing_remaining_time": str(remaining_time)
} }
for time_item in ["time_start", "time_end"]:
if not sorted_chan_guide[channel]["listing"][0][time_item]:
chan_dict["listing_%s" % time_item] = "N/A"
elif str(sorted_chan_guide[channel]["listing"][0][time_item]).endswith(tuple(["+0000", "+00:00"])):
chan_dict["listing_%s" % time_item] = str(sorted_chan_guide[channel]["listing"][0][time_item])
else:
chan_dict["listing_%s" % time_item] = str(datetime.datetime.fromtimestamp(sorted_chan_guide[channel]["listing"][0][time_item]))
if source in self.fhdhr.origins.valid_origins:
chan_obj = self.fhdhr.device.channels.get_channel_obj("origin_id", sorted_chan_guide[channel]["id"], source)
chan_dict["name"] = chan_obj.dict["name"]
chan_dict["number"] = chan_obj.number
chan_dict["chan_thumbnail"] = chan_obj.thumbnail
chan_dict["enabled"] = chan_obj.dict["enabled"]
chan_dict["m3u_url"] = chan_obj.api_m3u_url
chan_dict["listing_thumbnail"] = chan_dict["listing_thumbnail"] or chan_obj.thumbnail
else:
if not chan_dict["listing_thumbnail"]:
chan_dict["listing_thumbnail"] = chan_dict["chan_thumbnail"]
if not chan_dict["listing_thumbnail"]:
chan_dict["listing_thumbnail"] = "/api/images?method=generate&type=channel&message=%s" % chan_dict["number"]
chan_guide_list.append(chan_dict) chan_guide_list.append(chan_dict)
return render_template('guide.html', request=request, fhdhr=self.fhdhr, chan_guide_list=chan_guide_list, epg_methods=epg_methods) return render_template('guide.html', request=request, session=session, fhdhr=self.fhdhr, chan_guide_list=chan_guide_list, epg_methods=epg_methods, source=source, list=list)

View File

@ -1,9 +1,11 @@
from flask import request, render_template from flask import request, render_template, session
class Index_HTML(): class Index_HTML():
endpoints = ["/index", "/index.html"] endpoints = ["/index", "/index.html"]
endpoint_name = "page_index_html" endpoint_name = "page_index_html"
endpoint_access_level = 0
pretty_name = "Index"
def __init__(self, fhdhr): def __init__(self, fhdhr):
self.fhdhr = fhdhr self.fhdhr = fhdhr
@ -13,15 +15,13 @@ class Index_HTML():
def get(self, *args): def get(self, *args):
tuners_in_use = self.fhdhr.device.tuners.inuse_tuner_count() origin = self.fhdhr.origins.valid_origins[0]
max_tuners = self.fhdhr.device.tuners.max_tuners
fhdhr_status_dict = { fhdhr_status_dict = {
"Script Directory": str(self.fhdhr.config.internal["paths"]["script_dir"]), "Script Directory": str(self.fhdhr.config.internal["paths"]["script_dir"]),
"Config File": str(self.fhdhr.config.config_file), "Config File": str(self.fhdhr.config.config_file),
"Cache Path": str(self.fhdhr.config.internal["paths"]["cache_dir"]), "Cache Path": str(self.fhdhr.config.internal["paths"]["cache_dir"]),
"Total Channels": len(self.fhdhr.device.channels.list), "Total Channels": len(list(self.fhdhr.device.channels.list[origin].keys())),
"Tuner Usage": ("%s/%s" % (str(tuners_in_use), str(max_tuners))),
} }
return render_template('index.html', request=request, fhdhr=self.fhdhr, fhdhr_status_dict=fhdhr_status_dict, list=list) return render_template('index.html', request=request, session=session, fhdhr=self.fhdhr, fhdhr_status_dict=fhdhr_status_dict, list=list)

View File

@ -1,18 +0,0 @@
from flask import request, render_template
class Origin_HTML():
endpoints = ["/origin", "/origin.html"]
endpoint_name = "page_origin_html"
def __init__(self, fhdhr):
self.fhdhr = fhdhr
def __call__(self, *args):
return self.get(*args)
def get(self, *args):
origin_status_dict = self.fhdhr.originwrapper.get_status_dict()
origin_status_dict["Total Channels"] = len(self.fhdhr.device.channels.list)
return render_template('origin.html', request=request, fhdhr=self.fhdhr, origin_status_dict=origin_status_dict, list=list)

View File

@ -1,9 +1,12 @@
from flask import request, render_template from flask import request, render_template, session
class Settings_HTML(): class Settings_HTML():
endpoints = ["/settings", "/settings.html"] endpoints = ["/settings", "/settings.html"]
endpoint_name = "page_settings_html" endpoint_name = "page_settings_html"
endpoint_access_level = 1
endpoint_category = "tool_pages"
pretty_name = "Settings"
def __init__(self, fhdhr): def __init__(self, fhdhr):
self.fhdhr = fhdhr self.fhdhr = fhdhr
@ -19,15 +22,12 @@ class Settings_HTML():
for config_item in list(self.fhdhr.config.conf_default[config_section].keys()): for config_item in list(self.fhdhr.config.conf_default[config_section].keys()):
if self.fhdhr.config.conf_default[config_section][config_item]["config_web"]: if self.fhdhr.config.conf_default[config_section][config_item]["config_web"]:
real_config_section = config_section
if config_section == self.fhdhr.config.dict["main"]["dictpopname"]:
real_config_section = "origin"
web_settings_dict[config_section][config_item] = { web_settings_dict[config_section][config_item] = {
"value": self.fhdhr.config.dict[real_config_section][config_item], "value": self.fhdhr.config.dict[config_section][config_item],
"value_default": self.fhdhr.config.conf_default[config_section][config_item]["value"], "value_default": self.fhdhr.config.conf_default[config_section][config_item]["value"],
"hide": self.fhdhr.config.conf_default[config_section][config_item]["config_web_hidden"] "hide": self.fhdhr.config.conf_default[config_section][config_item]["config_web_hidden"]
} }
if not len(web_settings_dict[config_section].keys()): if not len(web_settings_dict[config_section].keys()):
del web_settings_dict[config_section] del web_settings_dict[config_section]
return render_template('settings.html', request=request, fhdhr=self.fhdhr, web_settings_dict=web_settings_dict, list=list) return render_template('settings.html', request=request, session=session, fhdhr=self.fhdhr, web_settings_dict=web_settings_dict, list=list)

View File

@ -1,4 +1,4 @@
from flask import request, render_template from flask import request, render_template, session
from fHDHR.tools import humanized_filesize from fHDHR.tools import humanized_filesize
@ -6,6 +6,9 @@ from fHDHR.tools import humanized_filesize
class Tuners_HTML(): class Tuners_HTML():
endpoints = ["/tuners", "/tuners.html"] endpoints = ["/tuners", "/tuners.html"]
endpoint_name = "page_streams_html" endpoint_name = "page_streams_html"
endpoint_access_level = 0
endpoint_category = "tool_pages"
pretty_name = "Tuners"
def __init__(self, fhdhr): def __init__(self, fhdhr):
self.fhdhr = fhdhr self.fhdhr = fhdhr
@ -15,22 +18,36 @@ class Tuners_HTML():
def get(self, *args): def get(self, *args):
tuner_list = [] tuner_status_dict = {}
tuner_status = self.fhdhr.device.tuners.status() tuner_status = self.fhdhr.device.tuners.status()
tuner_scanning = 0 for origin in list(tuner_status.keys()):
for tuner in list(tuner_status.keys()): tuner_status_dict[origin] = {}
tuner_status_dict[origin]["scan_count"] = 0
tuner_status_dict[origin]["status_list"] = []
for tuner in list(tuner_status[origin].keys()):
if tuner_status[origin][tuner]["status"] == "Scanning":
tuner_status_dict[origin]["scan_count"] += 1
tuner_dict = { tuner_dict = {
"number": str(tuner), "number": str(tuner),
"status": str(tuner_status[tuner]["status"]), "status": str(tuner_status[origin][tuner]["status"]),
"origin": "N/A",
"channel_number": "N/A",
"method": "N/A",
"running_time": "N/A",
"downloaded": "N/A",
} }
if tuner_status[tuner]["status"] == "Active":
tuner_dict["channel_number"] = tuner_status[tuner]["channel"]
tuner_dict["method"] = tuner_status[tuner]["method"]
tuner_dict["play_duration"] = str(tuner_status[tuner]["Play Time"])
tuner_dict["downloaded"] = humanized_filesize(tuner_status[tuner]["downloaded"])
elif tuner_status[tuner]["status"] == "Scanning":
tuner_scanning += 1
tuner_list.append(tuner_dict) if tuner_status[origin][tuner]["status"] in ["Active", "Acquired", "Scanning"]:
tuner_dict["origin"] = tuner_status[origin][tuner]["origin"]
tuner_dict["channel_number"] = tuner_status[origin][tuner]["channel"] or "N/A"
tuner_dict["running_time"] = str(tuner_status[origin][tuner]["running_time"])
return render_template('tuners.html', request=request, fhdhr=self.fhdhr, tuner_list=tuner_list, tuner_scanning=tuner_scanning) if tuner_status[origin][tuner]["status"] in "Active":
tuner_dict["method"] = tuner_status[origin][tuner]["method"]
tuner_dict["downloaded"] = humanized_filesize(tuner_status[origin][tuner]["downloaded"])
tuner_status_dict[origin]["status_list"].append(tuner_dict)
return render_template('tuners.html', request=request, session=session, fhdhr=self.fhdhr, tuner_status_dict=tuner_status_dict, list=list)

View File

@ -1,9 +1,12 @@
from flask import request, render_template from flask import request, render_template, session
class Version_HTML(): class Version_HTML():
endpoints = ["/version", "/version.html"] endpoints = ["/version", "/version.html"]
endpoint_name = "page_version_html" endpoint_name = "page_version_html"
endpoint_access_level = 1
endpoint_category = "tool_pages"
pretty_name = "Version"
def __init__(self, fhdhr): def __init__(self, fhdhr):
self.fhdhr = fhdhr self.fhdhr = fhdhr
@ -12,7 +15,19 @@ class Version_HTML():
return self.get(*args) return self.get(*args)
def get(self, *args): def get(self, *args):
version_dict = {} version_dict = {}
for key in list(self.fhdhr.config.internal["versions"].keys()): for key in list(self.fhdhr.config.internal["versions"].keys()):
version_dict[key] = self.fhdhr.config.internal["versions"][key] version_dict[key] = self.fhdhr.config.internal["versions"][key]
return render_template('version.html', request=request, fhdhr=self.fhdhr, version_dict=version_dict, list=list)
# Sort the Version Info
sorted_version_list = sorted(version_dict, key=lambda i: (version_dict[i]['type'], version_dict[i]['name']))
sorted_version_dict = {
"fHDHR": version_dict["fHDHR"],
"fHDHR_web": version_dict["fHDHR_web"]
}
for version_item in sorted_version_list:
if version_item not in ["fHDHR", "fHDHR_web"]:
sorted_version_dict[version_item] = version_dict[version_item]
return render_template('version.html', request=request, session=session, fhdhr=self.fhdhr, version_dict=sorted_version_dict, list=list)

View File

@ -1,9 +1,12 @@
from flask import request, render_template from flask import request, render_template, session
class xmlTV_HTML(): class xmlTV_HTML():
endpoints = ["/xmltv", "/xmltv.html"] endpoints = ["/xmltv", "/xmltv.html"]
endpoint_name = "page_xmltv_html" endpoint_name = "page_xmltv_html"
endpoint_access_level = 1
endpoint_category = "tool_pages"
pretty_name = "xmltv"
def __init__(self, fhdhr): def __init__(self, fhdhr):
self.fhdhr = fhdhr self.fhdhr = fhdhr
@ -13,4 +16,4 @@ class xmlTV_HTML():
def get(self, *args): def get(self, *args):
return render_template('xmltv.html', request=request, fhdhr=self.fhdhr) return render_template('xmltv.html', request=request, session=session, fhdhr=self.fhdhr, list=list)

View File

@ -1,58 +0,0 @@
from flask import Response, request
from io import BytesIO
import xml.etree.ElementTree
from fHDHR.tools import sub_el
class RMG_Device_XML():
endpoints = ["/rmg/device.xml"]
endpoint_name = "rmg_device_xml"
def __init__(self, fhdhr):
self.fhdhr = fhdhr
def __call__(self, *args):
return self.get(*args)
def get(self, *args):
"""Device.xml referenced from SSDP"""
base_url = request.url_root[:-1]
out = xml.etree.ElementTree.Element('root')
out.set('xmlns', "urn:schemas-upnp-org:device-1-0")
specVersion_out = sub_el(out, 'specVersion')
sub_el(specVersion_out, 'major', "1")
sub_el(specVersion_out, 'minor', "0")
device_out = sub_el(out, 'device')
sub_el(device_out, 'deviceType', "urn:plex-tv:device:Media:1")
sub_el(device_out, 'friendlyName', self.fhdhr.config.dict["fhdhr"]["friendlyname"])
sub_el(device_out, 'manufacturer', self.fhdhr.config.dict["fhdhr"]["reporting_manufacturer"])
sub_el(device_out, 'manufacturerURL', "https://github.com/fHDHR/%s" % self.fhdhr.config.dict["main"]["reponame"])
sub_el(device_out, 'modelName', self.fhdhr.config.dict["fhdhr"]["reporting_model"])
sub_el(device_out, 'modelNumber', self.fhdhr.config.internal["versions"]["fHDHR"])
sub_el(device_out, 'modelDescription', self.fhdhr.config.dict["fhdhr"]["friendlyname"])
sub_el(device_out, 'modelURL', "https://github.com/fHDHR/%s" % self.fhdhr.config.dict["main"]["reponame"])
serviceList_out = sub_el(device_out, 'serviceList')
service_out = sub_el(serviceList_out, 'service')
sub_el(out, 'URLBase', "%s" % base_url)
sub_el(service_out, 'serviceType', "urn:plex-tv:service:MediaGrabber:1")
sub_el(service_out, 'serviceId', "urn:plex-tv:serviceId:MediaGrabber")
sub_el(device_out, 'UDN', "uuid:%s" % self.fhdhr.config.dict["main"]["uuid"])
fakefile = BytesIO()
fakefile.write(b'<?xml version="1.0" encoding="UTF-8"?>\n')
fakefile.write(xml.etree.ElementTree.tostring(out, encoding='UTF-8'))
device_xml = fakefile.getvalue()
return Response(status=200,
response=device_xml,
mimetype='application/xml')

View File

@ -1,47 +0,0 @@
from flask import Response
from io import BytesIO
import xml.etree.ElementTree
from fHDHR.tools import sub_el
class RMG_Devices_DeviceKey_Channels():
endpoints = ["/devices/<devicekey>/channels", "/rmg/devices/<devicekey>/channels"]
endpoint_name = "rmg_devices_devicekey_channels"
endpoint_methods = ["GET"]
def __init__(self, fhdhr):
self.fhdhr = fhdhr
def __call__(self, devicekey, *args):
return self.get(devicekey, *args)
def get(self, devicekey, *args):
"""Returns the current channels."""
out = xml.etree.ElementTree.Element('MediaContainer')
if devicekey == self.fhdhr.config.dict["main"]["uuid"]:
out.set('size', str(len(self.fhdhr.device.channels.list)))
for fhdhr_id in list(self.fhdhr.device.channels.list.keys()):
channel_obj = self.fhdhr.device.channels.list[fhdhr_id]
if channel_obj.enabled:
sub_el(out, 'Channel',
drm="0",
channelIdentifier="id://%s" % channel_obj.dict["number"],
name=channel_obj.dict["name"],
origin=channel_obj.dict["callsign"],
number=str(channel_obj.dict["number"]),
type="tv",
# TODO param
signalStrength="100",
signalQuality="100",
)
fakefile = BytesIO()
fakefile.write(b'<?xml version="1.0" encoding="UTF-8"?>\n')
fakefile.write(xml.etree.ElementTree.tostring(out, encoding='UTF-8'))
device_xml = fakefile.getvalue()
return Response(status=200,
response=device_xml,
mimetype='application/xml')

View File

@ -1,31 +0,0 @@
from flask import request, redirect
import urllib.parse
class RMG_Devices_DeviceKey_Media():
endpoints = ["/devices/<devicekey>/media/<channel>", "/rmg/devices/<devicekey>/media/<channel>"]
endpoint_name = "rmg_devices_devicekey_media"
endpoint_methods = ["GET"]
def __init__(self, fhdhr):
self.fhdhr = fhdhr
def __call__(self, devicekey, channel, *args):
return self.get(devicekey, channel, *args)
def get(self, devicekey, channel, *args):
param = request.args.get('method', default=None, type=str)
self.fhdhr.logger.debug("param:%s" % param)
method = self.fhdhr.config.dict["fhdhr"]["stream_type"]
redirect_url = "/api/tuners?method=%s" % (method)
if str(channel).startswith('id://'):
channel = str(channel).replace('id://', '')
redirect_url += "&channel=%s" % str(channel)
redirect_url += "&accessed=%s" % urllib.parse.quote(request.url)
return redirect(redirect_url)

View File

@ -1,49 +0,0 @@
from flask import Response, request
from io import BytesIO
import xml.etree.ElementTree
from fHDHR.tools import sub_el
class RMG_Devices_Discover():
endpoints = ["/devices/discover", "/rmg/devices/discover"]
endpoint_name = "rmg_devices_discover"
endpoint_methods = ["GET", "POST"]
def __init__(self, fhdhr):
self.fhdhr = fhdhr
def __call__(self, *args):
return self.get(*args)
def get(self, *args):
"""This endpoint requests the grabber attempt to discover any devices it can, and it returns zero or more devices."""
base_url = request.url_root[:-1]
out = xml.etree.ElementTree.Element('MediaContainer')
out.set('size', "1")
sub_el(out, 'Device',
key=self.fhdhr.config.dict["main"]["uuid"],
make=self.fhdhr.config.dict["fhdhr"]["reporting_manufacturer"],
model=self.fhdhr.config.dict["fhdhr"]["reporting_model"],
modelNumber=self.fhdhr.config.internal["versions"]["fHDHR"],
protocol="livetv",
status="alive",
title=self.fhdhr.config.dict["fhdhr"]["friendlyname"],
tuners=str(self.fhdhr.config.dict["fhdhr"]["tuner_count"]),
uri=base_url,
uuid="device://tv.plex.grabbers.fHDHR/%s" % self.fhdhr.config.dict["main"]["uuid"],
thumb="favicon.ico",
interface='network'
# TODO add preferences
)
fakefile = BytesIO()
fakefile.write(b'<?xml version="1.0" encoding="UTF-8"?>\n')
fakefile.write(xml.etree.ElementTree.tostring(out, encoding='UTF-8'))
device_xml = fakefile.getvalue()
return Response(status=200,
response=device_xml,
mimetype='application/xml')

View File

@ -1,51 +0,0 @@
from flask import Response, request
from io import BytesIO
import xml.etree.ElementTree
from fHDHR.tools import sub_el
class RMG_Devices_Probe():
endpoints = ["/devices/probe", "/rmg/devices/probe"]
endpoint_name = "rmg_devices_probe"
endpoint_methods = ["GET", "POST"]
def __init__(self, fhdhr):
self.fhdhr = fhdhr
def __call__(self, *args):
return self.get(*args)
def get(self, *args):
"""Probes a specific URI for a network device, and returns a device, if it exists at the given URI."""
base_url = request.url_root[:-1]
uri = request.args.get('uri', default=None, type=str)
out = xml.etree.ElementTree.Element('MediaContainer')
out.set('size', "1")
if uri == base_url:
sub_el(out, 'Device',
key=self.fhdhr.config.dict["main"]["uuid"],
make=self.fhdhr.config.dict["fhdhr"]["reporting_manufacturer"],
model=self.fhdhr.config.dict["fhdhr"]["reporting_model"],
modelNumber=self.fhdhr.config.internal["versions"]["fHDHR"],
protocol="livetv",
status="alive",
title=self.fhdhr.config.dict["fhdhr"]["friendlyname"],
tuners=str(self.fhdhr.config.dict["fhdhr"]["tuner_count"]),
uri=base_url,
uuid="device://tv.plex.grabbers.fHDHR/%s" % self.fhdhr.config.dict["main"]["uuid"],
thumb="favicon.ico",
interface='network'
)
fakefile = BytesIO()
fakefile.write(b'<?xml version="1.0" encoding="UTF-8"?>\n')
fakefile.write(xml.etree.ElementTree.tostring(out, encoding='UTF-8'))
device_xml = fakefile.getvalue()
return Response(status=200,
response=device_xml,
mimetype='application/xml')

View File

@ -1,57 +1,74 @@
<html> <html>
<head> <head>
<title>{{ fhdhr.config.dict["fhdhr"]["friendlyname"] }}</title> <title>{{ fhdhr.config.dict["fhdhr"]["friendlyname"] }}</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
table, th, td {border: 1px solid black;}
</style>
<link href="style.css" rel="stylesheet"> <link href="style.css" rel="stylesheet">
<script type="text/javascript" src="/brython.js"></script>
<script type="text/javascript" src="/brython_stdlib.js"></script>
<script type="text/python" src="brython.bry"></script>
</head> </head>
<body>
<h1 style="text-align: center;"> <body onload="brython({debug: 1, indexedDB: false})">
<h1 class="center" style="text-align:center">
<span style="text-decoration: underline;"><strong><em>{{ fhdhr.config.dict["fhdhr"]["friendlyname"] }}</em></strong> <span style="text-decoration: underline;"><strong><em>{{ fhdhr.config.dict["fhdhr"]["friendlyname"] }}</em></strong>
</span> </span>
<img class="pull-left" src="/favicon.ico" alt="fHDHR Logo" width="100" height="100"> <img class="pull-left" src="/favicon.ico" alt="fHDHR Logo" width="100" height="100">
</h1> </h1>
{% set retmessage = request.args.get('retmessage', default=None) %}
{% if retmessage %}
<p></p>
<p class="pull-left" style="font-size:1em">{{ retmessage }}</p>
{% endif %}
<br><br> <br><br>
<p></p> <p></p>
<div>
<button class="pull-left" onclick="OpenLink('/')">fHDHR</a></button> <div>
<button class="pull-left" onclick="OpenLink('/origin')">{{ fhdhr.config.dict["main"]["servicename"] }}</a></button> <button onclick="location.href='/index'" type="button">fHDHR</button>
<button class="pull-left" onclick="OpenLink('/channels')">Channels</a></button>
<button class="pull-left" onclick="OpenLink('/guide')">Guide</a></button> {% for page_dict in session["route_list"]["pages"] %}
<button class="pull-left" onclick="OpenLink('/cluster')">Cluster/SSDP</a></button> {% if session["route_list"]["pages"][page_dict]["name"] != "page_index_html" and fhdhr.config.dict["web_ui"]["access_level"] >= session["route_list"]["pages"][page_dict]["endpoint_access_level"] %}
<button class="pull-left" onclick="OpenLink('/tuners')">Tuners</a></button> <button onclick="location.href='{{ session["route_list"]["pages"][page_dict]["endpoints"][0] }}'" type="button">{{ session["route_list"]["pages"][page_dict]["pretty_name"] }}</button>
<button class="pull-left" onclick="OpenLink('/xmltv')">xmltv</a></button> {% endif %}
<button class="pull-left" onclick="OpenLink('/version')">Version</a></button> {% endfor %}
<button class="pull-left" onclick="OpenLink('/diagnostics')">Diagnostics</a></button>
<button class="pull-left" onclick="OpenLink('/settings')">Settings</a></button>
<a class="pull-right" style="padding: 5px;" href="/api/xmltv?method=get&source={{ fhdhr.device.epg.def_method }}">xmltv</a> <a class="pull-right" style="padding: 5px;" href="/api/xmltv?method=get&source={{ fhdhr.device.epg.def_method }}">xmltv</a>
<a class="pull-right" style="padding: 5px;" href="/api/m3u?method=get&channel=all">m3u</a> <a class="pull-right" style="padding: 5px;" href="/api/m3u?method=get&channel=all">m3u</a>
<a class="pull-right" style="padding: 5px;" href="/api/w3u?method=get&channel=all">w3u</a>
</div> <form class="pull-right" style="padding: 5px;" method="post" action="/api/settings?method=update&redirect={{ request.path }}">
<input type="hidden" name="config_section" value="web_ui">
<input type="hidden" name="config_name" value="access_level">
{% if fhdhr.config.dict["web_ui"]["access_level"] == 2 %}
<input type="hidden" name="config_value" value=0>
{% elif fhdhr.config.dict["web_ui"]["access_level"] == 1 %}
<input type="hidden" name="config_value" value=2>
{% else %}
<input type="hidden" name="config_value" value=1>
{% endif %}
<input type="hidden" name="config_default" value=0>
{% if fhdhr.config.dict["web_ui"]["access_level"] == 2 %}
<a><input type="submit" value="Extra Advanced"></a>
{% elif fhdhr.config.dict["web_ui"]["access_level"] == 1 %}
<a><input type="submit" value="Advanced"></a>
{% else %}
<a><input type="submit" value="Basic"></a>
{% endif %}
</form>
<hr align="center" width="100%"> <hr align="center" width="100%">
</div>
{% set locations = fhdhr.device.cluster.get_cluster_dicts_web() %} {% for page_dict in session["route_list"]["tool_pages"] %}
{% if locations %} {% if session["route_list"]["tool_pages"][page_dict]["name"] != "page_index_html" and fhdhr.config.dict["web_ui"]["access_level"] >= session["route_list"]["tool_pages"][page_dict]["endpoint_access_level"] %}
<div> <button onclick="location.href='{{ session["route_list"]["tool_pages"][page_dict]["endpoints"][0] }}'" type="button">{{ session["route_list"]["tool_pages"][page_dict]["pretty_name"] }}</button>
{% for location in locations %} {% endif %}
<button class="pull-left" onclick="OpenLink('{{ location["base_url"] }}')">{{ location["name"] }}</a></button>
{% endfor %} {% endfor %}
</div>
<hr align="center" width="100%">
{% endif %}
{% set retmessage = request.args.get('retmessage', default=None) %}
{% if retmessage %}
<p>{{ retmessage }}</p>
{% endif %}
{% block content %}{% endblock %} {% block content %}{% endblock %}
</body> </body>
</html> </html>
<script>
function OpenLink(NewURL) {window.open(NewURL, "_self");}
</script>

View File

@ -4,30 +4,40 @@
<h4 style="text-align: center;">{{ fhdhr.config.dict["fhdhr"]["friendlyname"] }} Channels</h4> <h4 style="text-align: center;">{{ fhdhr.config.dict["fhdhr"]["friendlyname"] }} Channels</h4>
<p>
{% for origin in origin_methods %}
<button onclick="location.href='/channels?origin={{ origin }}'" type="button">{{ origin }}</button>
{% endfor %}
</p>
<div style="text-align: center;"> <div style="text-align: center;">
<button onclick="OpenLink('/api/tuners?method=scan&redirect=%2Fchannels')">Force Channel Update</a></button><p> Note: This may take some time.</p> <button onclick="location.href='/api/tuners?method=scan&origin={{ origin }}&redirect=/channels?origin={{ origin }}'" type="button">Force Channel Update</button>
<p> Note: This may take some time.</p>
</div> </div>
<br> <br>
<table class="center" style="width:50%"> <div class="container">
<tr> <table class="table-small center">
<th></th> <tbody>
<th></th>
</tr>
{% for key in list(channels_dict.keys()) %} {% for key in list(channels_dict.keys()) %}
<tr> <tr>
<td>{{ key }}</td> <td>{{ key }}</td>
<td>{{ channels_dict[key] }}</td> <td>{{ channels_dict[key] }}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody>
</table>
</div>
<br>
<div style="text-align: center;"> <div style="text-align: center;">
<button onclick="OpenLink('/channels_editor')">Edit Channels</a></button> <button onclick="location.href='/channels_editor?origin={{ origin }}'" type="button">Edit Channels</button>
</div> </div>
<br> <br>
<table class="center" style="width:100%"> <div class="container">
<table class="table-scroll center small-first-col">
<thead>
<tr> <tr>
<th>Play</th> <th>Play</th>
<th>Channel Name</th> <th>Channel Name</th>
@ -37,14 +47,17 @@
<th>Enabled</th> <th>Enabled</th>
<th>Favorite</th> <th>Favorite</th>
</tr> </tr>
</thead>
<tbody class="body-half-screen">
{% for chan_dict in channelslist %} {% for chan_dict in channelslist %}
<tr> <tr>
<td> <td>
{% if chan_dict["enabled"] %} {% if chan_dict["enabled"] %}
<a href="{{ chan_dict["play_url"] }}">Play</a> <a href="{{ chan_dict["m3u_url"] }}">Play</a>
{% else %}
<a href="{{ chan_dict["m3u_url"] }}" style="visibility:hidden">Play</a>
{% endif %} {% endif %}
</td> </td>
<td>{{ chan_dict["name"] }}</td> <td>{{ chan_dict["name"] }}</td>
@ -68,7 +81,9 @@
{% else %} {% else %}
<td>No</td> <td>No</td>
{% endif %} {% endif %}
</tr>
{% endfor %} {% endfor %}
</tbody>
</table>
</div>
{% endblock %} {% endblock %}

View File

@ -3,13 +3,39 @@
{% block content %} {% block content %}
<h4 style="text-align: center;">{{ fhdhr.config.dict["fhdhr"]["friendlyname"] }} Channels Editor</h4> <h4 style="text-align: center;">{{ fhdhr.config.dict["fhdhr"]["friendlyname"] }} Channels Editor</h4>
<input type="hidden" id="origin" value="{{ origin }}">
<div style="text-align: center;"> <div class="container">
<button onclick="OpenLink('/api/channels?method=enable&channel=all&redirect=%2Fchannels_editor')">Enable All</a></button> <table class="table-medium center">
<button onclick="OpenLink('/api/channels?method=disable&channel=all&redirect=%2Fchannels_editor')">Disable All</a></button> <tbody>
<tr>
<td>
<form id="Chan_Edit_Enable_Toggle" method="post">
<button type="button" id="enable_button" value="0">Disable All</button>
</form>
</td>
<td>
<form id="Chan_Edit_Favorite_Toggle" method="post">
<button type="button" id="favorite_button" value="0">Unfavorite All</button>
</form>
</td>
<td>
<form id="Chan_Edit_Modify" method="post">
<button type="Submit" id="modify_all">Modify All</button>
</form>
<form id="Chan_Edit_Reset" method="post">
<button type="Submit" id="reset_all">Reset All</button>
</form>
</td>
</tr>
</tbody>
</table>
</div> </div>
<table class="center" style="width:100%"> <div class="container">
<table class="table-scroll center text-edit-cols">
<thead>
<tr> <tr>
<th>Channel Name</th> <th>Channel Name</th>
<th>Channel CallSign</th> <th>Channel CallSign</th>
@ -17,55 +43,58 @@
<th>Channel Thumbnail</th> <th>Channel Thumbnail</th>
<th>Enabled</th> <th>Enabled</th>
<th>Favorite</th> <th>Favorite</th>
<th>Update</th> <th>Actions</th>
<th>Reset</th>
</tr> </tr>
</thead>
<tbody class="body-half-screen">
{% for chan_dict in channelslist %} {% for chan_dict in channelslist %}
<tr>
<input type="hidden" name="id" class="channels" value="{{ chan_dict["id"] }}">
<input type="hidden" class="reset" name="id" value="{{ chan_dict["id"] }}">
<td><input type="text" class="channels" name="name" value="{{ chan_dict["name"] }}" placeholder="{{ chan_dict["name"] }}"></td>
<input type="hidden" class="reset" name="name" value="{{ chan_dict["origin_name"] }}" placeholder="{{ chan_dict["name"] }}">
<td><input type="text" class="channels" name="callsign" value="{{ chan_dict["callsign"] }}" placeholder="{{ chan_dict["callsign"] }}"></td>
<input type="hidden" class="reset" name="callsign" value="{{ chan_dict["origin_callsign"] }}" placeholder="{{ chan_dict["callsign"] }}">
<td><input type="text" class="channels" name="number" value="{{ chan_dict["number"] }}" placeholder="{{ chan_dict["number"] }}"></td>
<input type="hidden" class="reset" name="number" value="{{ chan_dict["origin_number"] }}" placeholder="{{ chan_dict["number"] }}">
<td><input type="text" class="channels" name="thumbnail" value="{{ chan_dict["thumbnail"] }}" placeholder="{{ chan_dict["thumbnail"] }}"></td>
<input type="hidden" class="reset" name="thumbnail" value="{{ chan_dict["origin_thumbnail"] }}" placeholder="{{ chan_dict["thumbnail"] }}">
<form method="post" action="/api/channels?method=update&redirect=%2Fchannels_editor">
<input type="hidden" name="id" value={{ chan_dict["id"] }}>
<td data-th="Channel Name"><input type="text" name="name" value={{ chan_dict["name"] }}></td>
<td data-th="Channel Calsign"><input type="text" name="callsign" value={{ chan_dict["callsign"] }}></td>
<td data-th="Channel Number"><input type="text" name="number" value={{ chan_dict["number"] }}></td>
<td data-th="Channel Thumbnail"><input type="text" name="thumbnail" value={{ chan_dict["thumbnail"] }}></td>
<td>
<select name="enabled">
{% if chan_dict["enabled"] %} {% if chan_dict["enabled"] %}
<option value=True selected>Enabled</option> <td><input type="checkbox" class="channels" name="enabled" value=True checked placeholder="{{ chan_dict["enabled"] }}"></td>
<option value=False>Disabled</option>
{% else %} {% else %}
<option value=True>Enabled</option> <td><input type="checkbox" class="channels" name="enabled" value=True placeholder="{{ chan_dict["enabled"] }}"></td>
<option value=False selected>Disabled</option>
{% endif %} {% endif %}
</select> <input type="hidden" class="reset" name="enabled" value=True placeholder="{{ chan_dict["enabled"] }}">
</td>
<td>
<select name="favorite">
{% if chan_dict["favorite"] %} {% if chan_dict["favorite"] %}
<option value=1 selected>Yes</option> <td><input type="checkbox" class="channels" name="favorite" value=1 checked placeholder="{{ chan_dict["favorite"] }}"></td>
<option value=0>No</option>
{% else %} {% else %}
<option value=1>Yes</option> <td><input type="checkbox" class="channels" name="favorite" value=1 placeholder="{{ chan_dict["favorite"] }}"></td>
<option value=0 selected>No</option>
{% endif %} {% endif %}
</select> <input type="hidden" class="reset" name="favorite" value="0" placeholder="{{ chan_dict["favorite"] }}">
</td>
<td data-th="Update"><input type="submit" value="Update"></td> <td>
<form id="Chan_Edit_Modify" method="post">
<button type="Submit" id="modify_{{ chan_dict["id"] }}">Modify</button>
</form> </form>
<form method="post" action="/api/channels?method=update&redirect=%2Fchannels_editor"> <form id="Chan_Edit_Reset" method="post">
<input type="hidden" name="id" value={{ chan_dict["id"] }}> <button type="Submit" id="reset_{{ chan_dict["id"] }}">Reset</button>
<input type="hidden" name="name" value={{ chan_dict["origin_name"] }}>
<input type="hidden" name="callsign" value={{ chan_dict["origin_callsign"] }}>
<input type="hidden" name="number" value={{ chan_dict["origin_number"] }}>
<input type="hidden" name="thumbnail" value={{ chan_dict["origin_thumbnail"] }}>
<input type="hidden" name="enabled" value=True>
<td data-th="Reset"><input type="submit" value="Reset"></td>
</form> </form>
</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody>
</table>
<input type="hidden" name="id" class="channels" value="dummy">
<input type="hidden" class="reset" name="id" value="dummy">
</div>
{% endblock %} {% endblock %}

View File

@ -1,51 +0,0 @@
{% extends "base.html" %}
{% block content %}
<h4 style="text-align: center;">Cluster/SSDP</h4>
{% if not fhdhr.config.dict["fhdhr"]["discovery_address"] %}
<p style="text-align: center;">Discovery Address must be set for Cluster/SSDP</p>
{% else %}
<div style="text-align: center;">
<button onclick="OpenLink('/api/cluster?method=scan&redirect=%2Fcluster')">Force Scan</a></button>
<button onclick="OpenLink('/api/cluster?method=alive&redirect=%2Fcluster')">Send Alive</a></button>
<button onclick="OpenLink('/api/cluster?method=disconnect&redirect=%2Fcluster')">Disconnect</a></button>
</div>
<br>
<table class="center" style="width:50%">
<tr>
<th>Name</th>
<th>Location</th>
<th>Joined</th>
<th>Options</th>
</tr>
{% for location in locations_list %}
<tr>
<td>{{ location["name"] }}</td>
<td>{{ location["location"] }}</td>
<td>{{ location["joined"] }}</td>
<td>
<div>
{% if location["joined"] in ["True", "False"] %}
<button onclick="OpenLink('{{ location["location"] }}')">Visit</a></button>
{% endif %}
{% if location["joined"] == "True" %}
<button onclick="OpenLink('/api/cluster?method=del&location={{ location["url_query"] }}&redirect=%2Fcluster')">Remove</a></button>
{% elif location["joined"] == "False" %}
<button onclick="OpenLink('/api/cluster?method=add&location={{ location["url_query"] }}&redirect=%2Fcluster')">Add</a></button>
{% endif %}
</div>
</td>
</tr>
{% endfor %}
{% endif %}
{% endblock %}

View File

@ -4,33 +4,35 @@
<h4 style="text-align: center;">fHDHR Diagnostic Links</h4> <h4 style="text-align: center;">fHDHR Diagnostic Links</h4>
<table class="center" style="width:100%"> {% for route_group in list(button_dict.keys()) %}
<tr>
<th>Item</th>
<th>HDHR</th>
<th>RMG</th>
<th>Non-Specific</th>
</tr>
{% for button_item in button_list %} <h4 style="text-align: center;">{{ route_group }}</h4>
<div class="container">
<table class="table-settings center action-col text-edit-cols">
<thead>
<tr> <tr>
<td>{{ button_item["label"] }}</td> <th>Link</th>
{% if button_item["hdhr"] %} <th>Methods</th>
<td><button onclick="OpenLink('{{ button_item["hdhr"] }}')">{{ button_item["label"] }}</a></button></td>
{% else %}
<td></td>
{% endif %}
{% if button_item["rmg"] %}
<td><button onclick="OpenLink('{{ button_item["rmg"] }}')">{{ button_item["label"] }}</a></button></td>
{% else %}
<td></td>
{% endif %}
{% if button_item["other"] %}
<td><button onclick="OpenLink('{{ button_item["other"] }}')">{{ button_item["label"] }}</a></button></td>
{% else %}
<td></td>
{% endif %}
</tr> </tr>
</thead>
<tbody>
{% for button_item in button_dict[route_group] %}
<tr>
<td>
{% if button_item["button"] %}
<button onclick="location.href='{{ button_item["link"] }}'" type="button">{{ button_item["label"] }}</button>
{% else %}
<a>{{ button_item["link"] }}</a>
{% endif %}
</td>
<td><a>{{ button_item["methods"] }}</a></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endfor %} {% endfor %}
{% endblock %} {% endblock %}

View File

@ -6,31 +6,54 @@
<p> <p>
{% for epg_method in epg_methods %} {% for epg_method in epg_methods %}
<button onclick="OpenLink('/guide?source={{ epg_method }}')">{{ epg_method }}</a></button> <button onclick="location.href='/guide?source={{ epg_method }}'" type="button">{{ epg_method }}</button>
{% endfor %} {% endfor %}
</p> </p>
<table class="center" style="width:100%"> <div class="container">
<table class="table-scroll">
<thead>
<tr> <tr>
{% if source in fhdhr.origins.valid_origins %}
<th>Play</th>
{% endif %}
<th>Channel Name</th> <th>Channel Name</th>
<th>Channel Number</th> <th>Channel Number</th>
<th>Channel Thumbnail</th> <th>Channel Thumbnail</th>
<th>Content Title</th> <th>Content Title</th>
<th>Content Thumbnail</th> <th>Content Thumbnail</th>
<th>Content Description</th> <th>Content Description</th>
<th>Start Time (UTC)</th>
<th>End Time (UTC)</th>
<th>Content Remaining Time</th> <th>Content Remaining Time</th>
</tr> </tr>
</thead>
<tbody class="body-half-screen">
{% for chan_dict in chan_guide_list %} {% for chan_dict in chan_guide_list %}
<tr> <tr>
{% if source in fhdhr.origins.valid_origins %}
<td>
{% if chan_dict["enabled"] %}
<a href="{{ chan_dict["m3u_url"] }}">Play</a>
{% else %}
<a href="{{ chan_dict["m3u_url"] }}" style="visibility:hidden">Play</a>
{% endif %}
</td>
{% endif %}
<td>{{ chan_dict["name"] }}</td> <td>{{ chan_dict["name"] }}</td>
<td>{{ chan_dict["number"] }}</td> <td>{{ chan_dict["number"] }}</td>
<td><img src="{{ chan_dict["chan_thumbnail"] }}" alt="{{ chan_dict["name"] }}" width="100" height="100"></td> <td><img src="{{ chan_dict["chan_thumbnail"] }}" alt="{{ chan_dict["name"] }}" width="100" height="100"></td>
<td>{{ chan_dict["listing_title"] }}</td> <td>{{ chan_dict["listing_title"] }}</td>
<td><img src="{{ chan_dict["listing_thumbnail"] }}" alt="{{ chan_dict["listing_title"] }}" width="100" height="100"></td> <td><img src="{{ chan_dict["listing_thumbnail"] }}" alt="{{ chan_dict["listing_title"] }}" width="100" height="100"></td>
<td>{{ chan_dict["listing_description"] }}</td> <td>{{ chan_dict["listing_description"] }}</td>
<td>{{ chan_dict["remaining_time"] }}</td> <td>{{ chan_dict["listing_time_start"] }}</td>
<td>{{ chan_dict["listing_time_end"] }}</td>
<td>{{ chan_dict["listing_remaining_time"] }}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody>
</table>
</div>
{% endblock %} {% endblock %}

View File

@ -4,17 +4,19 @@
<h4 style="text-align: center;">fHDHR Status</h4> <h4 style="text-align: center;">fHDHR Status</h4>
<table class="center" style="width:50%">
<tr> <div class="container">
<th></th> <table class="table-medium center">
<th></th> <tbody>
</tr>
{% for key in list(fhdhr_status_dict.keys()) %} {% for key in list(fhdhr_status_dict.keys()) %}
<tr> <tr>
<td>{{ key }}</td> <td class="rTableCell">{{ key }}</td>
<td>{{ fhdhr_status_dict[key] }}</td> <td class="rTableCell">{{ fhdhr_status_dict[key] }}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody>
</table>
</div>
{% endblock %} {% endblock %}

View File

@ -1,20 +0,0 @@
{% extends "base.html" %}
{% block content %}
<h4 style="text-align: center;">{{ fhdhr.config.dict["main"]["servicename"] }} Status</h4>
<table class="center" style="width:50%">
<tr>
<th></th>
<th></th>
</tr>
{% for key in list(origin_status_dict.keys()) %}
<tr>
<td>{{ key }}</td>
<td>{{ origin_status_dict[key] }}</td>
</tr>
{% endfor %}
{% endblock %}

View File

@ -4,56 +4,64 @@
<h4 style="text-align: center;">fHDHR Settings</h4> <h4 style="text-align: center;">fHDHR Settings</h4>
<h4 style="text-align: center;">Settings will require a manual restart.</h4> <h4 style="text-align: center;">Some Settings will require a manual restart.</h4>
<div style="text-align: center;">
<button onclick="location.href='/api/settings?method=restart&redirect=/settings'" type="button">Restart fHDHR</button>
<p> Note: This may take some time, and you will have to refresh your page.</p>
</div>
<br>
{% for config_section in list(web_settings_dict.keys()) %} {% for config_section in list(web_settings_dict.keys()) %}
{% if config_section == "origin" %}
<h4 style="text-align: center;">{{ fhdhr.config.dict["main"]["dictpopname"] }}</h4>
{% else %}
<h4 style="text-align: center;">{{ config_section }}</h4> <h4 style="text-align: center;">{{ config_section }}</h4>
{% endif %}
<table class="center" style="width:100%">
<div class="container">
<table class="table-settings center action-col text-edit-cols">
<thead>
<tr> <tr>
<th>Config Name</th> <th>Config Name</th>
<th>Config Default Value</th> <th>Config Default Value</th>
<th>Config Value</th> <th>Config Value</th>
<th>Update</th> <th>Action</th>
<th>Reset</th>
</tr> </tr>
</thead>
<tbody>
{% for config_item in list(web_settings_dict[config_section].keys()) %} {% for config_item in list(web_settings_dict[config_section].keys()) %}
<tr> <tr>
<td data-th="Config Name">{{ config_item }}</td> <td>{{ config_item }}</td>
<td>{{ web_settings_dict[config_section][config_item]["value_default"] }}</td>
<td data-th="Config Default Value">{{ web_settings_dict[config_section][config_item]["value_default"] }}</td> <td>
<form method="post" action="/api/settings?method=update&redirect=/settings">
<form method="post" action="/api/settings?method=update&redirect=%2Fsettings"> <input type="hidden" name="config_section" value="{{ config_section }}">
<input type="hidden" name="config_section" value={{ config_section }}> <input type="hidden" name="config_name" value="{{ config_item }}">
<input type="hidden" name="config_name" value={{ config_item }}> <input type="hidden" name="config_default" value="{{ web_settings_dict[config_section][config_item]["value_default"] }}">
<input type="hidden" name="config_default" value={{ web_settings_dict[config_section][config_item]["value_default"] }}>
{% if web_settings_dict[config_section][config_item]["hide"] %} {% if web_settings_dict[config_section][config_item]["hide"] %}
<td data-th="Config Value"><input type="text" size="50" name="config_value" value=**************></td> <input type="text" size="25" name="config_value" value="**************">
{% else %} {% else %}
<td data-th="Config Value"><input type="text" size="50" name="config_value" value={{ web_settings_dict[config_section][config_item]["value"] }}></td> <input type="text" size="25" name="config_value" value="{{ web_settings_dict[config_section][config_item]["value"] }}">
{% endif %} {% endif %}
<td data-th="Update"><input type="submit" value="Update"></td> </td>
<td style="display:flex;">
<span style="margin:auto">
<input type="submit" value="Update">
</form> </form>
</span>
<form method="post" action="/api/settings?method=update&redirect=%2Fsettings"> <form style="margin:auto">
<input type="hidden" name="config_section" value={{ config_section }}> <input type="hidden" name="config_section" value="{{ config_section }}">
<input type="hidden" name="config_name" value={{ config_item }}> <input type="hidden" name="config_name" value="{{ config_item }}">
<input type="hidden" name="config_value" value={{ web_settings_dict[config_section][config_item]["value_default"] }}> <input type="hidden" name="config_value" value="{{ web_settings_dict[config_section][config_item]["value_default"] }}">
<input type="hidden" name="config_default" value={{ web_settings_dict[config_section][config_item]["value_default"] }}> <input type="hidden" name="config_default" value="{{ web_settings_dict[config_section][config_item]["value_default"] }}">
<td data-th="Reset"><input type="submit" value="Reset"></td> <input type="submit" value="Reset">
</form> </form>
</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody>
</table> </table>
</div>
{% endfor %} {% endfor %}

View File

@ -4,46 +4,48 @@
<h4 style="text-align: center;">fHDHR Streams</h4> <h4 style="text-align: center;">fHDHR Streams</h4>
<table class="center" style="width:100%"> {% for origin in list(tuner_status_dict.keys()) %}
<h4 style="text-align: center;">{{ origin }}</h4>
<div class="container">
<table class="table-medium center action-col">
<tbody>
<tr> <tr>
<th>Tuner</th> <th>Tuner</th>
<th>Status</th> <th>Status</th>
<th>Origin</th>
<th>Channel</th> <th>Channel</th>
<th>Method</th> <th>Method</th>
<th>Time Active</th> <th>Time Active</th>
<th>Total Downloaded</th> <th>Total Downloaded</th>
<th>Options</th> <th>Actions</th>
</tr> </tr>
{% for tuner_dict in tuner_list %} {% for tuner_dict in tuner_status_dict[origin]["status_list"] %}
<tr> <tr>
<td>{{ tuner_dict["number"] }}</td> <td>{{ tuner_dict["number"] }}</td>
<td>{{ tuner_dict["status"] }}</td> <td>{{ tuner_dict["status"] }}</td>
{% if tuner_dict["status"] in ["Active", "Acquired"] %} <td>{{ tuner_dict["origin"] }}</td>
<td>{{ tuner_dict["channel_number"] }}</td> <td>{{ tuner_dict["channel_number"] }}</td>
{% else %}
<td>N/A</td>
{% endif %}
{% if tuner_dict["status"] == "Active" %}
<td>{{ tuner_dict["method"] }}</td> <td>{{ tuner_dict["method"] }}</td>
<td>{{ tuner_dict["play_duration"] }}</td> <td>{{ tuner_dict["running_time"] }}</td>
<td>{{ tuner_dict["downloaded"] }}</td> <td>{{ tuner_dict["downloaded"] }}</td>
{% else %}
<td>N/A</td>
<td>N/A</td>
<td>N/A</td>
{% endif %}
<td> <td>
<div>
{% if tuner_dict["status"] != "Inactive" %} {% if tuner_dict["status"] != "Inactive" %}
<button onclick="OpenLink('/api/tuners?method=close&tuner={{ tuner_dict["number"] }}&redirect=%2Ftuners')">Close</a></button> <button onclick="location.href='/api/tuners?method=close&tuner={{ tuner_dict["number"] }}&origin={{ origin }}&redirect=/tuners'" type="button">Close</button>
{% endif %} {% endif %}
{% if not tuner_scanning and tuner_dict["status"] == "Inactive" %} {% if not tuner_status_dict[origin]["scan_count"] and tuner_dict["status"] == "Inactive" %}
<button onclick="OpenLink('/api/tuners?method=scan&tuner={{ tuner_dict["number"] }}&redirect=%2Ftuners')">Channel Scan</a></button> <button onclick="location.href='/api/tuners?method=scan&tuner={{ tuner_dict["number"] }}&origin={{ origin }}&redirect=/tuners'" type="button">Channel Scan</button>
{% endif %} {% endif %}
</div>
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody>
</table>
</div>
{% endfor %}
{% endblock %} {% endblock %}

View File

@ -4,17 +4,19 @@
<h4 style="text-align: center;">fHDHR Version Information</h4> <h4 style="text-align: center;">fHDHR Version Information</h4>
<table class="center" style="width:50%">
<tr>
<th></th>
<th></th>
</tr>
<div class="container">
<table class="table-medium center">
<tbody>
{% for key in list(version_dict.keys()) %} {% for key in list(version_dict.keys()) %}
<tr> <tr>
<td>{{ version_dict[key]["type"] }}</td>
<td>{{ key }}</td> <td>{{ key }}</td>
<td>{{ version_dict[key] }}</td> <td>{{ version_dict[key]["version"] }}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody>
</table>
</div>
{% endblock %} {% endblock %}

View File

@ -4,35 +4,30 @@
<h4 style="text-align: center;">xmltv</h4> <h4 style="text-align: center;">xmltv</h4>
<table class="center" style="width:50%"> <div class="container">
<table class="table-medium center action-col">
<tbody>
<tr> <tr>
<th>Version</th> <th>Version</th>
<th>XMLTV Link</th> <th>XMLTV Link</th>
<th>EPG Link</th> <th>EPG Link</th>
<th>Options</th> <th>Actions</th>
</tr> </tr>
{% for epg_method in fhdhr.config.dict["main"]["valid_epg_methods"] %} {% for epg_method in list(fhdhr.config.dict["epg"]["valid_methods"].keys()) %}
{% if epg_method not in [None, "None"] %} {% if epg_method not in [None, "None"] %}
{% set epg_method_name = epg_method %}
{% if epg_method == "origin" %}
{% set epg_method_name = fhdhr.config.dict["main"]["dictpopname"] %}
{% endif %}
<tr> <tr>
<td>{{ epg_method_name }}</td> <td> {{ epg_method }}</td>
<td><a href="/api/xmltv?method=get&source={{ epg_method }}">{{ epg_method_name }}</a></td> <td><a href="/api/xmltv?method=get&source={{ epg_method }}">{{ epg_method }}</a></td>
<td><a href="/api/epg?method=get&source={{ epg_method }}">{{ epg_method_name }}</a></td> <td><a href="/api/epg?method=get&source={{ epg_method }}">{{ epg_method }}</a></td>
<td> <td>
<div> <button onclick="location.href='/api/xmltv?method=update&source={{ epg_method }}&redirect=/xmltv'" type="button">Update</button>
<button onclick="OpenLink('/api/xmltv?method=update&source={{ epg_method }}&redirect=%2Fxmltv')">Update</a></button> <button onclick="location.href='/api/xmltv?method=clearcache&source={{ epg_method }}&redirect=/xmltv'" type="button">Clear Cache</button>
<button onclick="OpenLink('/api/xmltv?method=clearcache&source={{ epg_method }}&redirect=%2Fxmltv')">Clear Cache</a></button>
</div>
</td> </td>
</tr> </tr>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
</tbody>
</table>
</div>
{% endblock %} {% endblock %}

View File

@ -4,6 +4,11 @@
"value": "none", "value": "none",
"config_file": true, "config_file": true,
"config_web": true "config_web": true
},
"access_level":{
"value": "0",
"config_file": true,
"config_web": true
} }
} }
} }

View File

@ -1,3 +1,7 @@
table {border: 1px solid black;}
th {border: 1px solid black;}
td {border: 1px solid black;}
.pull-right { float: right; } .pull-right { float: right; }
.pull-left { float: left; } .pull-left { float: left; }
@ -6,3 +10,170 @@
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
} }
.rTable {
display: table;
width: 100%;
}
.rTableRow {
display: table-row;
}
.rTableRowAlternate {
display: table-row;
background-color: #33FFFB;
}
.rTableHeading {
display: table-header-group;
background-color: #ddd;
font-weight: bold;
}
.rTableCell, .rTableHead {
display: table-cell;
padding: 3px 10px;
border: 1px solid #999999;
}
.rTableFoot {
display: table-footer-group;
font-weight: bold;
background-color: #ddd;
}
.rTableBody {
display: table-row-group;
max-height: 450px;
overflow-y: scroll;
}
.container{
padding: 1rem;
margin: 1rem;
}
.table-scroll{
/*width:100%; */
display: block;
empty-cells: show;
/* Decoration */
border-spacing: 0;
border: 1px solid;
}
.table-small{
width: 20%;
border-spacing: 0;
border: 1px solid;
}
.table-medium{
width: 50%;
border-spacing: 0;
border: 1px solid;
}
.table-large{
width: 75%;
border-spacing: 0;
border: 1px solid;
}
.table-settings{
width: 75%;
border-spacing: 0;
border: 1px solid;
}
.table-settings thead{
background-color: #f1f1f1;
position:relative;
display: block;
width:100%;
}
.table-settings tbody{
/* Position */
display: block; position:relative;
width:100%;
/* Decoration */
border-top: 1px solid rgba(0,0,0,0.2);
}
.table-settings tr{
width: 100%;
display:flex;
}
.table-settings td,.table-settings th{
flex: 1 1 0;
display: block;
padding: .25rem;
}
.table-scroll thead{
background-color: #f1f1f1;
position:relative;
display: block;
width:100%;
overflow-y: scroll;
}
.table-scroll tbody{
/* Position */
display: block; position:relative;
width:100%; overflow-y:scroll;
/* Decoration */
border-top: 1px solid rgba(0,0,0,0.2);
}
.table-scroll tr{
width: 100%;
display:flex;
}
.table-scroll td,.table-scroll th{
flex: 1 1 0;
display: block;
padding: .25rem;
text-align:center;
}
/* Other options */
.table-scroll.text-edit-cols td:nth-of-type(-n+4),
.table-scroll.text-edit-cols th:nth-of-type(-n+4){
flex: 2
}
.table-settings.text-edit-cols td:nth-of-type(-n+2),
.table-settings.text-edit-cols th:nth-of-type(-n+2){
flex: 1;
}
.table-scroll.small-first-col td:first-child,
.table-scroll.small-first-col th:first-child{
width: 20%;
flex: 0 1 0;
}
.action-col td:last-child{
flex:1;
}
.table-small tbody tr:nth-child(2n){
background-color: rgba(130,130,170,0.1);
}
.table-medium tbody tr:nth-child(2n){
background-color: rgba(130,130,170,0.1);
}
.table-large tbody tr:nth-child(2n){
background-color: rgba(130,130,170,0.1);
}
.table-scroll tbody tr:nth-child(2n){
background-color: rgba(130,130,170,0.1);
}
.body-half-screen{
max-height: 50vh;
}
.small-col{flex-basis:10%;}

View File

@ -6,15 +6,11 @@ monkey.patch_all()
import os import os
import sys import sys
import pathlib import pathlib
from multiprocessing import freeze_support
from fHDHR.cli import run from fHDHR.cli import run
import fHDHR_web import fHDHR_web
import alternative_epg
import origin
SCRIPT_DIR = pathlib.Path(os.path.dirname(os.path.abspath(__file__))) SCRIPT_DIR = pathlib.Path(os.path.dirname(os.path.abspath(__file__)))
if __name__ == '__main__': if __name__ == '__main__':
freeze_support() sys.exit(run.main(SCRIPT_DIR, fHDHR_web))
sys.exit(run.main(SCRIPT_DIR, fHDHR_web, origin, alternative_epg))

View File

@ -1,7 +0,0 @@
# pylama:ignore=W0401,W0611
from .origin_service import *
from .origin_channels import *
from .origin_epg import *
ORIGIN_NAME = "fHDHR_NextPVR"
ORIGIN_VERSION = "v0.5.0-beta"

View File

@ -1,61 +0,0 @@
import xmltodict
import json
class OriginChannels():
def __init__(self, fhdhr, origin):
self.fhdhr = fhdhr
self.origin = origin
def get_channel_thumbnail(self, channel_id):
channel_thumb_url = ("%s%s:%s/service?method=channel.icon&channel_id=%s" %
("https://" if self.fhdhr.config.dict["origin"]["ssl"] else "http://",
self.fhdhr.config.dict["origin"]["address"],
str(self.fhdhr.config.dict["origin"]["port"]),
str(channel_id)
))
return channel_thumb_url
def get_channels(self):
data_url = ('%s%s:%s/service?method=channel.list&sid=%s' %
("https://" if self.fhdhr.config.dict["origin"]["ssl"] else "http://",
self.fhdhr.config.dict["origin"]["address"],
str(self.fhdhr.config.dict["origin"]["port"]),
self.origin.sid
))
data_req = self.fhdhr.web.session.get(data_url)
data_dict = xmltodict.parse(data_req.content)
if 'channels' not in list(data_dict['rsp'].keys()):
self.fhdhr.logger.error("Could not retrieve channel list")
return []
channel_o_list = data_dict['rsp']['channels']['channel']
channel_list = []
for c in channel_o_list:
dString = json.dumps(c)
channel_dict = eval(dString)
clean_station_item = {
"name": channel_dict["name"],
"callsign": channel_dict["name"],
"number": channel_dict["formatted-number"],
"id": channel_dict["id"],
"thumbnail": self.get_channel_thumbnail(channel_dict["id"])
}
channel_list.append(clean_station_item)
return channel_list
def get_channel_stream(self, chandict):
streamurl = ('%s%s:%s/live?channel_id=%s&client=%s' %
("https://" if self.fhdhr.config.dict["origin"]["ssl"] else "http://",
self.fhdhr.config.dict["origin"]["address"],
str(self.fhdhr.config.dict["origin"]["port"]),
str(chandict["origin_id"]),
"fhdhr_" + str(chandict["origin_number"]),
))
return streamurl

Some files were not shown because too many files have changed in this diff Show More