Commit initial du projet serverIPTV

This commit is contained in:
ubuntu
2026-08-03 13:14:43 +00:00
commit a4b23c5f5d
13 changed files with 6906 additions and 0 deletions
+17
View File
@@ -0,0 +1,17 @@
.DS_Store
__pycache__/
*.pyc
.venv/
venv/
# Contenu téléchargé / généré, pas du code source
playlists/
playlists_download/
logos/
letsencrypt/
epg/epg.xml
static/config_init.db
static/device_snapshots/
# Config locale spécifique à cette machine
.claude/
+27
View File
@@ -0,0 +1,27 @@
FROM python
RUN apt-get update -y
ENV LANG C.UTF-8
ENV LC_ALL C.UTF-8
RUN apt-get -y install python3-pip
RUN apt-get install --reinstall ca-certificates
RUN pip3 install aiohttp
RUN pip3 install aiohttp-session
RUN pip3 install aiomcache
RUN pip3 install python-socketio
RUN pip3 install pyOpenSSL
RUN pip3 install tornado
RUN pip3 install aiohttp-cors
RUN pip3 install requests
RUN pip3 install pillow
RUN pip3 install pymongo
RUN pip3 install aiofiles
RUN pip3 install wikipedia-api
RUN pip3 install beautifulsoup4
EXPOSE 443
WORKDIR "/home"
CMD ["python3", "server.py"]
+37
View File
@@ -0,0 +1,37 @@
version: "3.8"
services:
mongodb:
image: mongo:latest
container_name: mongodb
ports:
- "27017:27017"
environment:
MONGO_INITDB_ROOT_USERNAME: root
MONGO_INITDB_ROOT_PASSWORD: example
volumes:
- mongodb_data:/data/db
networks:
- app-network
python-app:
build: .
container_name: serverIPTV-container
depends_on:
- mongodb
environment:
- MONGO_URI=mongodb://root:example@mongodb:27017/?authSource=admin
networks:
- app-network
volumes:
- /etc/letsencrypt/:/home/letsencrypt/:rw
- /home/ubuntu/serverIPTV/:/home/:rw
ports:
- "5023:5023"
- "443:443"
networks:
app-network:
volumes:
mongodb_data:
+60
View File
@@ -0,0 +1,60 @@
# Generic functions
def findInfo(tag, s):
end = '"'
if (s.find(tag)>-1):
t = s[s.find(tag)+len(tag)+2:s.rfind(end)]
return t[0:t.find(end)]
else:
return ""
def download_logo():
path_filename = 'source.m3u'
# Process the m3u file
f = open(path_filename, "r", encoding="utf8")
Lines = f.readlines()
print('ouverture fichier', flush=True)
channel = {}
separator = '--------'
topic = ''
num_channel = 0
for line in Lines:
#print(line,flush=True)
if separator in line:
topic = line[10:]
topic = topic.replace(separator,'')
if len(channel)>0 and line[0:4]=="http":
print ('download ' + channel['urlLogo'])
#new_channels.append(channel)
#print(channel, flush=True)
if line[0:4] == "#EXT":
channel = {}
# DEFINE CHANNEL NAME
name_channel = line[line.rfind(',')+1:].strip('\n')
channel_exist = False
if len(name_channel)>0:
channel = {"Name":name_channel, "Topic":topic, "urlLogo" : findInfo('tvg-logo', line)}
num_channel += 1
channel_exist = True
if channel_exist == False:
name_channel = line[line.rfind('|')+2:].strip('\n')
if len(name_channel) > 0:
channel = {"Name":name_channel, "Topic":topic, "urlLogo" : findInfo('tvg-logo', line)}
result = {'num_channel': num_channel}
return result
+21
View File
@@ -0,0 +1,21 @@
import requests
url = "http://xfvjwqjn.duperab.xyz/xmltv.php?username=VQNRZG7N&password=W3NFBDXY"
headers = {
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
"Accept-Language": "en-US,en;q=0.9,fr;q=0.8,fr-FR;q=0.7",
"Cache-Control": "max-age=0",
"Connection": "keep-alive",
"Upgrade-Insecure-Requests": "1",
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36 Edg/135.0.0.0"
}
response = requests.get(url, headers=headers, verify=False)
if response.status_code == 200 and response.content:
with open("epg.xml", "wb") as f:
f.write(response.content)
print("Download successful: epg.xml")
else:
print(f"Failed to download. Status code: {response.status_code}, Content length: {len(response.content)}")
+1488
View File
File diff suppressed because it is too large Load Diff
+1344
View File
File diff suppressed because it is too large Load Diff
+584
View File
@@ -0,0 +1,584 @@
var quote = String.fromCharCode(39)
var list_playlist = []
var result_search = []
var socket = io('https://iptv.mrk.ovh', {
secure: true,
rejectUnauthorized: false // Only for self-signed certificates
});
socket.on('connect', function() {
$('#sidClient').val(socket.id)
console.log("client connected sid = " + $('#sidClient').val())
});
socket.on('communication', function(data) {
console.log('communication', data)
// RECEPTION RETURN WARNING
if (data['typeCommand'] == 'warning') {
$('#warning').html(data['payload'])
$('#warning').show('slow')
setTimeout(() => { $('#warning').hide('slow') }, 5000);
}
})
$(window).on('scroll resize', lazyLoad);
// INITIALISAITON DES TABS
$('#tab_home').show()
update_list_playlist()
//get_search_channel()
$('#tab_search').hide()
$('#tab_playlist').hide()
$('#tab_devices').hide()
// CLICK TAB HOME
$('#display_tab_home').click(function(){
var triggerTab = new bootstrap.Tab($('#display_tab_home'));
triggerTab.show();
$('#tab_home').show()
$('#tab_search').hide()
$('#tab_playlist').hide()
$('#tab_devices').hide()
get_search_channel()
})
// CLICK TAB PLAYLIST
$('#display_tab_playlist').click(function(){
var triggerTab = new bootstrap.Tab($('#display_tab_playlist'));
triggerTab.show();
$('#tab_home').hide()
$('#tab_search').hide()
$('#tab_playlist').show()
$('#tab_devices').hide()
})
// CLICK TAB DEVICES
$('#display_tab_devices').click(function(){
var triggerTab = new bootstrap.Tab($('#display_tab_devices'));
triggerTab.show();
$('#tab_home').hide()
$('#tab_search').hide()
$('#tab_playlist').hide()
$('#tab_devices').show()
})
// CLICK TAB SEARCH
$('#display_tab_search').click(function(){
var triggerTab = new bootstrap.Tab($('#display_tab_devices'));
triggerTab.show();
$('#tab_home').hide()
$('#tab_search').show()
$('#tab_playlist').hide()
$('#tab_devices').hide()
})
// UPDATE TAB DEVICES
updateListDevices()
setInterval(updateListDevices, 10000)
$('#btn_search').click(function(){
get_search_channel($('#input_search').val())
})
$('#btn_initdb').click(function(){
$.get('init_db').done(function(data){
window.location.href = "/"
})
})
function isInViewport(element) {
const rect = element[0].getBoundingClientRect();
return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
);
}
function lazyLoad() {
$('.lazy').each(function () {
const img = $(this);
if (isInViewport(img) && !img.hasClass('loaded')) {
img.attr('src', img.data('src')); // Load the image
img.on('load', function () {
img.addClass('loaded'); // Add loaded class for fade-in
});
}
});
}
// DISPLAY CHANNELS
function displayResult() {
$('#listChannels').show()
if ($('#typeDisplay').val() == 'list') html = '<ul class="list-group">'
else html = '<div class="d-flex flex-wrap">'
result_search.forEach(element => {
var id_channel = element['id_channel']
var name_channel = element['Name']
var url_channel = element['url']
var url_channel_macos = url_channel.replace("http://", 'rtmp://')
//url_channel_macos = url_channel.replace("https://", 'rtmp://')
if ($('#typeDisplay').val() == 'list') {
html += `<li data-url_channel="${url_channel}" data-name_channel="${name_channel}" class="btn_play_media lineTable_device list-group-item"><img src="get_logo?id_channel=${id_channel}" style="height:30px"> <span class="p-3">${name_channel}</span></li>`
} else {
html += `<div data-id_channel="${id_channel}" class="m-2 card" style="width: 18rem;"> \
<img class="card-img-top mx-auto d-block mt-2" src="get_logo?id_channel=${id_channel}" alt="Card image cap" style='width:100px'> \
<div class="card-body"> \
<p class="btn_play_media card-text">${name_channel}</p> \
<button data-display_url="${url_channel}" class="btn_display_url btn btn-info">url</button> \
<a class="btn btn-primary" href="vlc://${url_channel_macos}">vlc ios</a> \
</div> \
</div>`
}
})
if ($('#typeDisplay').val() == 'list') html += '</ul>'
else html += '</div>'
$('#listChannels').html(html)
$('.btn_play_media').click(function(){
var name = $(this).data('name_channel')
var url = $(this).data('url_channel')
console.log('play media', name, url)
socket.emit('communication', {fromSidDevice : $('#sidClient').val(), toSidDevice: $('#sidDevice').val(), typeCommand : "on", payload: {name:name, url:url}, "packet":0});
})
$('.btn_display_url').click(function() {
var url = $(this).data('display_url')
alert(url)
})
}
// GET RESULT SEARCH
function get_search_channel() {
var keyword = $('#input_search').val()
if (keyword.length>0) {
$.post('list_channel', {keyword:encodeURIComponent(keyword)}).done(function(data){
$('#list_channels').html('')
var html =''
console.log(data)
result_search = JSON.parse(data)
displayResult()
lazyLoad();
})
}
}
// UPDATE LIST PLAYLIST
function update_list_playlist() {
$.get('get_list_playlists').done(function(data){
console.log(data)
$('#list_playlist').html('')
var html = '<table class="table"> \
<thead>\
<tr>\
<th scope="col">#</th>\
<th scope="col">Title</th>\
<th scope="col">End Date</th>\
<th scope="col">Num channels</th>\
<th scope="col"></th>\
<th scope="col"></th>\
<!--<th scope="col">Sid</th>-->\
</tr>\
</thead>\
<tbody>'
var dataJSON = JSON.parse(data)
//list_playlist = dataJSON
for (var i=0;i<dataJSON.length;i++) {
console.log(dataJSON[i])
var id_playlist = dataJSON[i].id_playlist
var state_playlist = dataJSON[i].activation
console.log('state playlist', state_playlist)
var html_state = ''
if (state_playlist == 1) {
html_state = 'class="btn btn-secondary btn-sm" >Active'
} else {
html_state = 'class="btn btn-outline-secondary btn-sm" >Inactive'
}
html += `<tr class="lineTable_device" id="zone_${dataJSON[i].title_playlist}">\
<th scope="row">${i.toString()}</th>\
<td>${dataJSON[i].title_playlist}</td>\
<td>${dataJSON[i].date_end_playlist}</td>\
<td>${dataJSON[i].num_channels}</td>\
<!--<td style="color:darkblue" id="zone_edit_playlist_${id_playlist}"><div class="d-flex flex-row"><div><i class="fas fa-edit"></i></div><div class="d-none d-lg-block" style="margin-left: 5px;">Edit</div></div></td>-->\
<td style="color:red" data-param1="id_playlist" id="zone_delete_playlist_${id_playlist}"><div class="d-flex flex-row"><div><i class="fas fa-trash-alt"></i></div><div class="d-none d-lg-block" style="margin-left: 5px;">Delete</div></div></td>\
<!--<td>'+dataJSON[i].id_playlist+'</td>-->\
<input id="state_activation_${id_playlist}" type="hidden" val=""> \
<td><div id="btn_activation_playlist_${id_playlist}" ${html_state}</div></td>\
</tr>`
}
html += ' </tbody></table>'
$('#list_playlist').html(html)
for (var i=0;i<dataJSON.length;i++) {
var id_playlist = dataJSON[i].id_playlist
var element = $('#zone_delete_playlist_'+id_playlist)
var state_playlist = dataJSON[i].activation
$("#state_activation_"+id_playlist).val(state_playlist)
element.click(function(){
var idp = $(this).attr('id').replace("zone_delete_playlist_", "")
console.log('delete playlist ' + idp)
$('#id_playlist_to_action').val(idp)
$('#modal_confirmation').modal('show')
})
$('#btn_activation_playlist_'+id_playlist).click(function(){
update_state_playlist(this)
})
}
function update_state_playlist(element) {
var id = $(element).attr('id').replace("btn_activation_playlist_", "");
console.log('change state from ' + $("#state_activation_"+id).val())
if ($("#state_activation_"+id).val() == "1") {
console.log('desactivate ' + id)
$.get('activation_playlist', {activation:0, id_playlist:id}).done(function(data){
var id_data = data['id_playlist']
$("#state_activation_"+id).val(0)
console.log(data)
$("#btn_activation_playlist_"+id).addClass("btn-outline-secondary")
$("#btn_activation_playlist_"+id).removeClass("btn-secondary")
$("#btn_activation_playlist_"+id).html("Inactive")
})
} else {
console.log('activate ' + id)
$.get('activation_playlist', {activation:1, id_playlist:id}).done(function(data){
var id_data = data['id_playlist']
console.log(data)
$("#state_activation_"+id_data).val(1)
$("#btn_activation_playlist_"+id_data).addClass("btn-secondary")
$("#btn_activation_playlist_"+id_data).removeClass("btn-outline-secondary")
$("#btn_activation_playlist_"+id_data).html("Active")
})
}
}
})
}
// UPDATE LIST DEVICES
function updateListDevices() {
$.get('list_devices').done(function (data) {
console.log('Data received')
var html = '<table class="table"> \
<thead>\
<tr>\
<th scope="col">#</th>\
<th scope="col">Device</th>\
<th scope="col">LastDateTime</th>\
<!--<th scope="col">Sid</th>-->\
<th scope="col">Channel</th>\
</tr>\
</thead>\
<tbody>'
var html2 = ''
var html3 = ''
var dataJSON = JSON.parse(data)
for (var i=0;i<dataJSON.length;i++) {
var sid_device = dataJSON[i].sid
var timestamp = dataJSON[i].last_datetime
var actualTS = Math.floor(Date.now() / 1000)
var delta = actualTS - timestamp
var status = ''
//var media = '<div class="alert alert-primary" role="alert">' + dataJSON[i].mediaRunning + '</div>'
var media = '<span class="badge bg-success">'+dataJSON[i].mediaRunning+'</span>'
if (delta < 60) {
status = '<span style="color: green;">Online</span>'
status2 = '#ebfaeb'
}
if (delta > 86400) {
status = '<span style="color: SlateBlue;">Offline</span>'
status2 = '#e6e6ff'
}
if ((delta > 60) && (delta <= 86400)) {
status = '<span style="color: red;">' + Math.floor(delta / 60).toString() + ' min</span>'
status2 = '#ffe6e6'
}
html += `<tr class="lineTable_device" data-name_device="${dataJSON[i].name}" data-sid_device="${sid_device}" >\
<th scope="row">${i.toString()}</th>\
<td>${dataJSON[i].name}</td>\
<td>${status}</td>\
<td>${media}</td>\
<!--<td>'+dataJSON[i].sid+'</td>-->\
</tr>`
html2 += '<li id="btn_select_device_'+dataJSON[i].id_device+'" data-param1="'+dataJSON[i].id_device+'" data-param2="'+dataJSON[i].name+'" data-param3="'+dataJSON[i].sid+'"><a class="dropdown-item" href="#">'+dataJSON[i].name+'</a></li>'
html3 += '<div id="btn_select_device_card_'+dataJSON[i].id_device+'" data-param1="'+dataJSON[i].id_device+'" data-param2="'+dataJSON[i].name+'" data-param3="'+dataJSON[i].sid+'" class="card card_device" style="background-color:'+status2+'"> \
<div class="card-body"> \
<p class="card-text"><div class="d-flex justify-content-between"> \
<div>'+dataJSON[i].name+'</div> \
<div><i class="fas fa-trash-alt"></i></div></div></p> \
<p class="card-text">'+status+'</p> \
<p class="card-text">'+media+'</p> \
</div> \
</div>'
}
html += ' </tbody></table>'
$('#listDevices').html(html)
//$('#list_device_home').html(html2)
//$('#zone_device_main').html(html3)
function configure_current_device(element) {
const id_device = $(element).data('param1');
const name_device = $(element).data('param2');
const sid = $(element).data('param3');
console.log(id_device, name_device)
$('#id_device').val(id_device)
$('#sidDevice').val(sid)
$('#btn_select_device').html(name_device)
}
for (var i=0;i<dataJSON.length;i++) {
$('#btn_select_device_'+dataJSON[i].id_device).click(function(){
configure_current_device(this)
})
$("#btn_select_device_card_"+dataJSON[i].id_device).click(function(){
configure_current_device(this)
})
}
})
}
$(document).on("click", ".lineTable_device", function() {
var sid = $(this).data('sid_device')
var name_device = $(this).data('name_device')
console.log("sid",sid)
$('#NameDevice').html(name_device)
$('#sidDevice').val(sid)
getChannelRunning()
channelInterval = setInterval(getChannelRunning, 5000)
})
function getChannelRunning() {
socket.emit('communication', {fromSidDevice : $('#sidClient').val(), toSidDevice: $('#sidDevice').val(), typeCommand : "getChannelRunning", payload: '', "packet":0});
}
// CLICK TAB HOME
$('#display_tab_home').click(function(){
var triggerTab = new bootstrap.Tab($('#display_tab_home'));
triggerTab.show();
$('#tab_home').show()
$('#tab_search').hide()
$('#tab_playlist').hide()
})
// CLICK TAB SEARCH
$('#display_tab_search').click(function(){
var triggerTab = new bootstrap.Tab($('#display_tab_search'));
triggerTab.show();
$('#tab_home').hide()
$('#tab_search').show()
$('#tab_playlist').hide()
})
// CLICK ADD PLAYLIST
$('#btn_configure_playlist').click(function(){
$('#modal_add_playlist').modal('show')
})
// Check valid URL
function isValidURL(string) {
var pattern = new RegExp('^(https?:\\/\\/)?' + // Protocol
'((([a-zA-Z0-9\\-]+\\.)+[a-zA-Z]{2,})|' + // Domain name
'localhost|' + // OR localhost
'\\d{1,3}(\\.\\d{1,3}){3})' + // OR IPv4
'(\\:\\d+)?' + // Optional port
'(\\/[-a-zA-Z0-9@:%._\\+~#=]*)*' + // Path
'(\\?[;&a-zA-Z0-9@:%._\\+~#=]*)?' + // Query string
'(\\#[-a-zA-Z0-9@:%._\\+~#=]*)?$', 'i'); // Fragment locator
return pattern.test(string);
}
// Update filemame
$("#file-input").on("change", function () {
$("#fileName").val(this.files[0]?.name || "No file selected.");
});
// CLICK BTN btn_confirmation_delete_playlist
$('#btn_confirmation_delete_playlist').click(function(){
$.post('delete_playlist', {id_playlist : $('#id_playlist_to_action').val() }).done(function(data){
console.log(data)
update_list_playlist()
$('#modal_confirmation').modal('hide')
})
})
// CLICK UPLOAD FILE PLAYLIST
$("#upload-button").click(function () {
// Validation form
var validation_date = false
if ($('#playlist_end_date').val()) validation_date = true; else validation_date = false;
if (!validation_date) {
$('#playlist_end_date').addClass('is-invalid');
$('#playlist_end_date').removeClass('is-valid');
} else {
$('#playlist_end_date').addClass('is-valid');
$('#playlist_end_date').removeClass('is-invalid');
}
var validation_url = isValidURL($('#playlist_url').val())
if (!validation_url) {
$('#playlist_url').addClass('is-invalid');
$('#playlist_url').removeClass('is-valid');
} else {
$('#playlist_url').addClass('is-valid');
$('#playlist_url').removeClass('is-invalid');
}
var validation_title = !($('#playlist_title').val().trim() === '')
if (!validation_title) {
$('#playlist_title').addClass('is-invalid');
$('#playlist_title').removeClass('is-valid');
} else {
$('#playlist_title').addClass('is-valid');
$('#playlist_title').removeClass('is-invalid');
}
if ((validation_title) && (validation_date) && (validation_url)) {
const file = $("#file-input")[0].files[0];
if (!file) {
$('#add_playlist_alert').html('<div class="alert alert-danger" role="alert">Please select a file</div>')
return;
}
const formData = new FormData();
formData.append("file", file);
const xhr = new XMLHttpRequest();
xhr.open("POST", "/upload", true);
// Update progress bar
xhr.upload.onprogress = function (event) {
if (event.lengthComputable) {
const percentComplete = Math.round((event.loaded / event.total) * 100);
$("#progress-bar").val(percentComplete);
$("#progress-text").text(percentComplete + "%");
}
};
xhr.onloadstart = function () {
$("#progress-container").show();
};
xhr.onload = function () {
if (xhr.status === 200) {
$('#add_playlist_alert').html('<div class="alert alert-success" role="alert">Upload successful!</div>')
var response =JSON.parse(xhr.response)
id_playlist = response['id_playlist']
if (response.status == 'ok') {
$.post('analyse_m3u_file', {'id_playlist':id_playlist,
'title':$('#playlist_title').val(),
'url':$('#playlist_url').val(),
'date_end':$('#playlist_end_date').val(),
'filename':$("#fileName").val()}).done(function(data){
var num_channel = data.num_channel
if (num_channel>0) {
$('#add_playlist_alert').html('<div class="alert alert-success" role="alert">Num channel : '+String(num_channel)+'</div>')
} else {
$('#add_playlist_alert').html('<div class="alert alert-danger" role="alert">No channel in the file</div>')
}
update_list_playlist()
$('#modal_add_playlist').modal('hide')
})
}
} else {
$('#add_playlist_alert').html('<div class="alert alert-danger" role="alert">Upload failed!</div>')
}
$("#progress-container").hide();
};
xhr.onerror = function () {
alert("An error occurred while uploading.");
$("#progress-container").hide();
};
xhr.send(formData);
}
});
$("#typeDisplay").change(function(){
console.log($("#typeDisplay").val())
displayResult()
})
$('#btn_display_player').click(function () {
if($('#player').css('display') == 'none'){
$('#player').show('slow');
$('#valRange').html='0'
$('#Range').val(0)
} else {
$('#player').hide('slow');
}
})
$(".dropdown-submenu .dropdown-toggle").on("click", function (e) {
e.preventDefault(); // Prevent link from navigating
e.stopPropagation(); // Prevent closing the whole dropdown
let $submenu = $(this).next(".dropdown-menu");
// Toggle the submenu visibility
$(".dropdown-submenu .dropdown-menu").not($submenu).removeClass("show"); // Close other submenus
$submenu.toggleClass("show");
});
// Close submenus when clicking outside
$(document).on("click", function (e) {
if (!$(e.target).closest(".dropdown-submenu").length) {
$(".dropdown-submenu .dropdown-menu").removeClass("show");
}
});
function off(){
console.log('OFF ',$('#sidDevice').val(), $('#sidClient').val())
socket.emit('communication', {fromSidDevice : $('#sidClient').val(), toSidDevice: $('#sidDevice').val(), typeCommand : "off", payload: 0, "packet":0});
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

+3255
View File
File diff suppressed because it is too large Load Diff
+73
View File
@@ -0,0 +1,73 @@
#!/bin/bash
set -e # Stop script on error
IPTV_SOURCE_URL="${IPTV_SOURCE_URL:-https://iptv.mrk.ovh/get_source}"
IPTV_DEST="/home/pi/Documents/IPTV.py"
CONFIG_DB_SOURCE_URL="${CONFIG_DB_SOURCE_URL:-https://iptv.mrk.ovh/static/config_init.db}"
CONFIG_DB_DEST="/home/pi/Documents/config.db"
RC_LOCAL="/etc/rc.local"
RC_LOCAL_CMD='sudo -u pi DISPLAY=:0 python3 /home/pi/Documents/IPTV.py &'
CURL_CMD=(curl -fsSL)
if [ "${CURL_INSECURE:-0}" = "1" ]; then
CURL_CMD+=( -k )
fi
echo "Updating system"
sudo apt update
echo "Installing packages"
sudo apt -y install vim openvpn vsftpd curl ca-certificates python3-pil.imagetk python3-tk
sudo update-ca-certificates
echo "Configuring vsftpd"
VSFTPD_CONF="/etc/vsftpd.conf"
sudo sed -i 's/^#write_enable=YES/write_enable=YES/' $VSFTPD_CONF
echo "Restarting vsftpd service"
sudo systemctl restart vsftpd
PYTHON_STDLIB_DIR="$(python3 -c 'import sysconfig; print(sysconfig.get_path("stdlib"))')"
echo "Fixing Python externally-managed restriction"
sudo rm -f "$PYTHON_STDLIB_DIR/EXTERNALLY-MANAGED"
echo "Installing Python packages"
sudo pip3 install python-vlc python-socketio wifi aiohttp websocket-client
echo "Downloading IPTV.py"
sudo mkdir -p /home/pi/Documents
sudo "${CURL_CMD[@]}" "$IPTV_SOURCE_URL" -o "$IPTV_DEST"
sudo chown pi:pi "$IPTV_DEST"
sudo chmod 755 "$IPTV_DEST"
echo "Downloading config database"
sudo "${CURL_CMD[@]}" "$CONFIG_DB_SOURCE_URL" -o "$CONFIG_DB_DEST"
sudo chown pi:pi "$CONFIG_DB_DEST"
sudo chmod 644 "$CONFIG_DB_DEST"
echo "Configuring rc.local"
if [ ! -f "$RC_LOCAL" ]; then
sudo tee "$RC_LOCAL" >/dev/null <<'EOF'
#!/bin/sh -e
exit 0
EOF
fi
if ! sudo grep -Fq "$RC_LOCAL_CMD" "$RC_LOCAL"; then
sudo sed -i "\$i\\
$RC_LOCAL_CMD
" "$RC_LOCAL"
fi
if ! sudo tail -n 1 "$RC_LOCAL" | grep -Fxq "exit 0"; then
echo "rc.local must end with 'exit 0'" >&2
exit 1
fi
sudo chmod 755 "$RC_LOCAL"
echo "All tasks completed successfully!"
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB