1642 lines
65 KiB
Python
1642 lines
65 KiB
Python
#!/usr/bin/python3
|
|
# -*- coding: utf-8 -*-
|
|
|
|
"""
|
|
IMM - Internet Media Manager: Play internet media in desktop media
|
|
player just like local files and optionally save them offline. IMM can
|
|
function as conventional media downloaders as well.
|
|
Copyright (C) 2018-2020 IMM@radii.dev
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU Affero General Public License as
|
|
published by the Free Software Foundation, either version 3 of the
|
|
License, or (at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU Affero General Public License for more details.
|
|
|
|
You should have received a copy of the GNU Affero General Public License
|
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
"""
|
|
|
|
"""
|
|
NOTE: IMPROVE CODE, FIX REPETIONS AND MESSES. GO THROUGH ENTIRE CODE. REFACTOR CODEBASE.
|
|
Use tuple where list is not required scuh as constant & non-global data.
|
|
|
|
Below, first several functions are defined. To read squentially, go to
|
|
the bottom and start reading from execution/calling of Main() function.
|
|
|
|
VD is VariablesDictionary. see GenerateVariablesDictionary() function.
|
|
"""
|
|
|
|
# Python standard library imports
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
import os
|
|
import threading
|
|
import glob
|
|
import shlex
|
|
import pathlib
|
|
|
|
# Third-party imports
|
|
# import youtube-dl
|
|
try:
|
|
import pyperclip
|
|
except:
|
|
print(
|
|
"WARNING: pyperclip module not found. Please install pyperclip"
|
|
" module https://pypi.org/project/pyperclip/ in your python "
|
|
"setup. It is required to automatically fetch urls from "
|
|
"clipboard. Loading pyperclip module provided in programme but"
|
|
" it might not work", file=sys.stderr)
|
|
try:
|
|
from Code.PythonModules import pyperclip
|
|
except:
|
|
# symbolic function if failed to import
|
|
class pyperclip:
|
|
def paste():
|
|
return ''
|
|
|
|
# Current programme imports
|
|
from Code.IMM.GeneralFunctions.InputOutput import (
|
|
ReadTextFile,
|
|
ReadlinesTextFile,
|
|
WriteGeneralTextFiles)
|
|
|
|
from Code.IMM.GeneralFunctions.DataStructureManupulations import (
|
|
ConvertToStandardPathFormat,
|
|
GetTextAfter,
|
|
DoubleQuoteString,
|
|
SingleQuoteString,
|
|
ListIntoString,
|
|
StandardVariableName)
|
|
|
|
|
|
class FindMediaURLs:
|
|
""" First look for media urls in programme arguments than clipboard
|
|
and finally ask for user input if still not found.
|
|
"""
|
|
def __init__(self):
|
|
MediaURLs = self.ExtractingMediaUrls(Arguments)
|
|
if len(MediaURLs) != 0:
|
|
self.MediaURLs = MediaURLs
|
|
return
|
|
|
|
# Causing Gtk-WARNING **: Theme parsing error:
|
|
Clipboard = pyperclip.paste()
|
|
Clipboard = Clipboard.split('\n')
|
|
MediaURLs = self.ExtractingMediaUrls(Clipboard)
|
|
if len(MediaURLs) != 0:
|
|
self.MediaURLs = MediaURLs
|
|
return
|
|
|
|
MediaURLs = self.ProcessUserInput()
|
|
self.MediaURLs = MediaURLs
|
|
|
|
def ProcessUserInput(self):
|
|
print(
|
|
"No media url found in aguments and clipboard. You can "
|
|
"paste media urls and imm created m3u files location "
|
|
"below. Paste one url per line (empty input will break the"
|
|
" url collection loop). This way you can paste list of "
|
|
"mutiple urls with one url per line (bulk url paste will "
|
|
"work)")
|
|
MediaURLList = ''
|
|
while len(MediaURLList) == 0:
|
|
UserInputList = []
|
|
while True:
|
|
UserInput = input()
|
|
UserInputList.append(UserInput)
|
|
if len(UserInput) == 0:
|
|
break
|
|
MediaURLList = self.ExtractingMediaUrls(UserInputList)
|
|
if len(MediaURLList) != 0:
|
|
return MediaURLList
|
|
print("No valid media url or m3u file cointaing valid "
|
|
"media url found. Try again!")
|
|
|
|
def ExtractingMediaUrls(self, Source):
|
|
MediaURLs = []
|
|
for i in range(len(Source)):
|
|
Item = Source[i].strip(Quotes + ' ')
|
|
if Item.casefold().startswith(VD['validmediaurlstart']):
|
|
MediaURL = DoubleQuoteString(Item)
|
|
MediaURLs.append(MediaURL)
|
|
elif Item.casefold().endswith(
|
|
VD['validextensioncontaningmediaurl1']):
|
|
|
|
M3UFile = ConvertToStandardPathFormat(Item)
|
|
MediaAvailableOffline = \
|
|
self.M3UFile_IsMediaAvailableOffline(M3UFile)
|
|
if len(MediaAvailableOffline) > 0:
|
|
MediaURLs.append(MediaAvailableOffline)
|
|
else:
|
|
MediaURL = self.GetMediaURLFromM3UFile(M3UFile)
|
|
if MediaURL != None:
|
|
MediaURL = DoubleQuoteString(MediaURL)
|
|
MediaURLs.append(MediaURL)
|
|
return MediaURLs
|
|
|
|
def GetMediaURLFromM3UFile(self, M3UFile):
|
|
if os.path.isfile(M3UFile) == True:
|
|
FileLines = ReadlinesTextFile(M3UFile)
|
|
MediaURL = GetTextAfter('#Source: ', FileLines).strip(' ')
|
|
if len(MediaURL) > 0:
|
|
return MediaURL
|
|
|
|
def M3UFile_IsMediaAvailableOffline(self, M3UFile):
|
|
Return = ''
|
|
FileLines = ReadlinesTextFile(M3UFile)
|
|
ChangeWorkingDirectoryTo = os.path.split(M3UFile)[0]
|
|
os.chdir(ChangeWorkingDirectoryTo)
|
|
for i in range(len(FileLines)):
|
|
Line = FileLines[i].strip('\n' + Quotes)
|
|
if Line.startswith('#') or\
|
|
Line.casefold().startswith(VD['validmediaurlstart']):
|
|
|
|
continue
|
|
else:
|
|
Line = os.path.abspath(Line)
|
|
if os.path.isfile(Line) == True:
|
|
Return ='Offline://' + Line
|
|
os.chdir(ProgrammeDirectory)
|
|
return Return
|
|
|
|
|
|
class CalculateMediaQualityPreference:
|
|
def __init__(self, DownloadOnly = False):
|
|
InternetSpeed = VD['internetspeed']
|
|
MediaQualityHeight = VD['mediaqualityheight']
|
|
PreferAudioOnly = VD['preferaudioonly']
|
|
self.AllowSeprateVideoAndAudioSource = True
|
|
if DownloadOnly == False:
|
|
if (len(MediaPlayerMultipleSourcePlayCommand) <= 0
|
|
or VD['allowseprateaudiosource'] != "yes"):
|
|
|
|
self.AllowSeprateVideoAndAudioSource = False
|
|
CustomFormatOptions = VD['customformatoptions'].strip(' "\'/')
|
|
if len(CustomFormatOptions) > 0:
|
|
CustomFormatOptions += '/'
|
|
SpeedBasedPreference = ''
|
|
QualityBasedPreference = ''
|
|
TryAudioFirst = ''
|
|
if InternetSpeed != None:
|
|
MaxAverageBitrate = 0.8 * int(InternetSpeed)
|
|
MaxAverageBitrateString = str(MaxAverageBitrate)
|
|
MaxAverageBitrate2 = MaxAverageBitrate - 320
|
|
MaxAverageBitrate2String = str(MaxAverageBitrate2)
|
|
SpeedBasedPreference = (
|
|
"bestvideo[tbr<="
|
|
+ MaxAverageBitrate2String
|
|
+ "]+bestaudio/worstvideo[tbr>="
|
|
+ MaxAverageBitrate2String
|
|
+ "]+bestaudio/best[tbr<="
|
|
+ MaxAverageBitrateString
|
|
+ "]/worst[tbr>="
|
|
+ MaxAverageBitrateString
|
|
+ "]/")
|
|
if MediaQualityHeight != None:
|
|
MediaQualityHeight = str(MediaQualityHeight)
|
|
QualityBasedPreference = (
|
|
"bestvideo[height<="
|
|
+ MediaQualityHeight
|
|
+ "]+bestaudio/worstvideo[height>="
|
|
+ MediaQualityHeight
|
|
+ "]+bestaudio/best[height<="
|
|
+ MediaQualityHeight
|
|
+ "]/worst[height>="
|
|
+ MediaQualityHeight
|
|
+ "]/")
|
|
if (PreferAudioOnly == 'yes'
|
|
or Arguments.count('--preferaudioonly') != 0):
|
|
|
|
if InternetSpeed != None:
|
|
TryAudioFirst = (
|
|
"bestaudio[tbr<="
|
|
+ MaxAverageBitrateString
|
|
+"']/worst[tbr>="
|
|
+ MaxAverageBitrateString
|
|
+ "]/")
|
|
else:
|
|
TryAudioFirst = "bestaudio/"
|
|
MediaPreference = (
|
|
TryAudioFirst
|
|
+ CustomFormatOptions
|
|
+ SpeedBasedPreference
|
|
+ QualityBasedPreference
|
|
+ "best")
|
|
if self.AllowSeprateVideoAndAudioSource == False:
|
|
MediaPreference = self.RemoveMutipleSourceFormats(
|
|
MediaPreference)
|
|
self.MediaPreference = MediaPreference
|
|
|
|
def RemoveMutipleSourceFormats(self, MediaPreference):
|
|
MediaPreferencelist = MediaPreference.split('/')
|
|
MediaPreference = []
|
|
for i in range(len(MediaPreferencelist)):
|
|
if MediaPreferencelist[i].find('+') == -1:
|
|
MediaPreference.append(MediaPreferencelist[i])
|
|
MediaPreference = '/'.join(MediaPreference)
|
|
return MediaPreference
|
|
|
|
def ManuallySelectFormat(self, MediaURL):
|
|
ManuallySelectFormat = VD['manuallyselectformat']
|
|
if (ManuallySelectFormat == 'yes'
|
|
or Arguments.count('--manuallyselectformat') != 0):
|
|
|
|
print('Finding available formats...')
|
|
GetAvailableFormatsCommand = ' '.join((
|
|
YoutubeDL,
|
|
"--list-formats --no-playlist",
|
|
MediaURL))
|
|
OutputString = RunSubprocessAndReturnOutputString(
|
|
GetAvailableFormatsCommand)
|
|
FoundFormats, FormatStartAt = self.DidFoundAnyFormat(
|
|
OutputString)
|
|
if FoundFormats == False:
|
|
print(
|
|
"Could not find any format. Press return key to "
|
|
" process next url in queue if any, else to exit. "
|
|
"Possible reasons: \n"
|
|
"1. Probably bug in IMM, report issue to us.\n"
|
|
"2. Unsupported website or supported but still not"
|
|
" working, report it to youtube-dl developers\n"
|
|
"3. Incorrect url or url has no media. check "
|
|
"again.\n"
|
|
"below is the output from youtube-dl:\n"
|
|
+ OutputString)
|
|
input()
|
|
return 'Failed'
|
|
InvalidInputTryAgainOrSkip = (
|
|
"Invalid format code. Try again or type 's' or 'skip'"
|
|
" to process media with automatic format selection.")
|
|
print(
|
|
"Type code of one (example 'videoandaudio') or two "
|
|
"(example 'videos+audio') of the available formats "
|
|
"below to process it. You can add a blank space ' ' "
|
|
"before to keep manuallyselectformat turned on, by "
|
|
"default its automaically turned off. Add blank space "
|
|
"' ' after code to add it to customformatoptions so "
|
|
"that you do not have to manully select it again and "
|
|
"again.\n"
|
|
+ OutputString[FormatStartAt:])
|
|
VerificationCode = -1
|
|
while VerificationCode == -1:
|
|
UserInput = input()
|
|
if len(UserInput) == 0 :
|
|
print(InvalidInputTryAgainOrSkip)
|
|
continue
|
|
if UserInput.startswith(' '):
|
|
KeepManuallySelectFormatON = True
|
|
else:
|
|
KeepManuallySelectFormatON = False
|
|
if UserInput.endswith(' '):
|
|
AddToCustomFormatOptions = True
|
|
else:
|
|
AddToCustomFormatOptions = False
|
|
UserInput = UserInput.strip(' ')
|
|
MultipleFormatSelected = UserInput.find('+')
|
|
if MultipleFormatSelected == -1:
|
|
Verification = '\n' + UserInput
|
|
VerificationCode = OutputString.find(Verification)
|
|
else:
|
|
if self.AllowSeprateVideoAndAudioSource == True:
|
|
FirstVerification = (
|
|
'\n'
|
|
+ UserInput[:MultipleFormatSelected])
|
|
FirstVerificationCode = OutputString.find(
|
|
FirstVerification)
|
|
LastVerification = (
|
|
'\n'
|
|
+ UserInput[(MultipleFormatSelected + 1):])
|
|
LastVerificationCode = OutputString.find(
|
|
LastVerification)
|
|
if FirstVerification != -1 and LastVerificationCode != -1:
|
|
VerificationCode = 1
|
|
else:
|
|
print(
|
|
"Media player do not support seprate "
|
|
"video+audio source playback. Select single"
|
|
" videoandaudio or audioonly format")
|
|
if VerificationCode != -1:
|
|
UserSelectedFormat = UserInput + '/'
|
|
if KeepManuallySelectFormatON == False:
|
|
Configuration.EditConfigurationFile(
|
|
'manuallyselectformat', 'no')
|
|
if AddToCustomFormatOptions == True:
|
|
AlreadyInConfiguration = \
|
|
VD['CustomFormatOptions'].strip('/ ')
|
|
UpdatedCustomFormatOptions = (
|
|
UserSelectedFormat
|
|
+ AlreadyInConfiguration).strip('/ ')
|
|
Configuration.EditConfigurationFile(
|
|
'CustomFormatOptions',
|
|
UpdatedCustomFormatOptions)
|
|
break
|
|
if UserInput == 's' or UserInput == 'skip':
|
|
UserSelectedFormat = ''
|
|
break
|
|
print(InvalidInputTryAgainOrSkip)
|
|
else:
|
|
UserSelectedFormat = ''
|
|
MediaPreference = DoubleQuoteString(
|
|
UserSelectedFormat + self.MediaPreference)
|
|
MediaPreference = "--format " + MediaPreference
|
|
return MediaPreference
|
|
|
|
def DidFoundAnyFormat(self, OutputString):
|
|
""" When ManuallySelectFormat is enabled, it tests if found any
|
|
format/media source links or not.
|
|
"""
|
|
FindTheseInSequence = (
|
|
'format', 'code', 'extension', 'resolution', 'note')
|
|
b = 0
|
|
for Item in range(len(FindTheseInSequence)):
|
|
a = OutputString[b:].find(FindTheseInSequence[Item])
|
|
if a == -1:
|
|
return False, 0
|
|
b += a
|
|
FormatStartAt = OutputString[:b].rfind('\n') + 1
|
|
return True, FormatStartAt
|
|
|
|
|
|
def ParseDomainName(MediaURL):
|
|
""" Parse domain name from url for naming m3u files in Previous
|
|
list and Watch Later list.
|
|
"""
|
|
DomainStart = MediaURL.find("//") + 2
|
|
MediaURLFactor2 = MediaURL[DomainStart:].find('/')
|
|
DomainEnd = DomainStart + MediaURLFactor2
|
|
MediaDomain = MediaURL[DomainStart : DomainEnd]
|
|
if MediaDomain.startswith("www."):
|
|
MediaDomain = MediaDomain[4:]
|
|
return MediaDomain
|
|
|
|
def ConvertDurationIntoSeconds(MediaDuration):
|
|
""" Example: Input:12:05 => Output: 725 """
|
|
DurationsList = MediaDuration.split(':')
|
|
DurationsListLength = len(DurationsList)
|
|
MutiplyingFactor = 1
|
|
DurationInSeconds = 0
|
|
for i in range(len(DurationsList) - 1, -1, -1):
|
|
Add = int(DurationsList[i]) * MutiplyingFactor
|
|
DurationInSeconds += Add
|
|
MutiplyingFactor *= 60
|
|
return str(DurationInSeconds)
|
|
|
|
class PlayOnly:
|
|
def __init__(self, MediaURL, MediaPreference):
|
|
self.MediaURL = MediaURL
|
|
self.MediaPreference = MediaPreference
|
|
""" This won't work it won't have custom configuration like
|
|
audio only, it will have seprate configuration, duplication
|
|
etc. and therefore defeating the purpose of IMM. Find how to
|
|
pass Arguments in gnome-mpv and how to send additonal media
|
|
urls to already playing gnome-mpv instance. No need of it any
|
|
since mpv itself don't have instance control either (not sure
|
|
it gnome-mpv have instance control or not yet. read
|
|
documentation on website or talk on IRC).
|
|
|
|
POSSIBLE FIX: read path of gnome-mpv's mpv configuration path
|
|
in gnome-mpv config folder and copy it and add arguments as
|
|
commands in copied file and specify that in argument of
|
|
execution of gnome-mpv. If no option to specify in arguments
|
|
than specify in configuration file and than execute it, than
|
|
after playing, restore gnome-mpv configuration file.
|
|
if no config file found than create it and than delete after
|
|
playing.
|
|
THIS CAN WORK.
|
|
Push changes upstream to support this feature.
|
|
"""
|
|
IsMediaPlayedAlready = self.DirectlyPlayForSomeMediaPlayers()
|
|
if IsMediaPlayedAlready != True:
|
|
self.MediaDomain = ParseDomainName(self.MediaURL)
|
|
PrimaryMethod = self.PrimaryPlayingMethod()
|
|
if PrimaryMethod == 'failed':
|
|
self.AlternatePlayingMethod()
|
|
|
|
# Kill this function
|
|
def DirectlyPlayForSomeMediaPlayers(self):
|
|
if (MediaPlayer.endswith('gnome-mpv')
|
|
or MediaPlayer.endswith('gnome-mpv.exe')):
|
|
|
|
PlayMedia(MediaURLListString)
|
|
return True
|
|
return False
|
|
|
|
def AlternatePlayingMethod(self):
|
|
print('Wait... extracting media url')
|
|
ExtractURL = ' '.join((
|
|
YoutubeDL,
|
|
"--get-url --no-playlist",
|
|
self.MediaPreference,
|
|
self.MediaURL))
|
|
ExtractedURL = RunSubprocessAndReturnOutputString(
|
|
ExtractURL).strip('\n')
|
|
QuoteExtractedURL = DoubleQuoteString(ExtractedURL)
|
|
StartProcessing.ProcessNextURL = True
|
|
PlayMedia(QuoteExtractedURL)
|
|
|
|
def PrimaryPlayingMethod(self):
|
|
print('Wait... extracting media url')
|
|
ExtractURLTitleAndDuration = ' '.join((
|
|
YoutubeDL,
|
|
"--get-title --get-url --get-duration --no-playlist",
|
|
AdditionalYoutubeDLArguments,
|
|
self.MediaPreference,
|
|
self.MediaURL))
|
|
ExtractedURLTitleAndDurationList = RunSubprocessAndReturnOutputString(
|
|
ExtractURLTitleAndDuration).strip('\n').split('\n')
|
|
LenOfExtractedURLTitleAndDurationList = \
|
|
len(ExtractedURLTitleAndDurationList)
|
|
ExtractedAudioURL = None
|
|
PrintErrorMessageIfFails = (
|
|
"Something went wrong with extraction process. Below "
|
|
"(or above) is the extracted string:\n"
|
|
+ ('\n').join(ExtractedURLTitleAndDurationList))
|
|
if LenOfExtractedURLTitleAndDurationList == 4:
|
|
if ExtractedURLTitleAndDurationList[2].find(
|
|
ExtractedURLTitleAndDurationList[1][:4]) != -1:
|
|
|
|
MediaTitle = ExtractedURLTitleAndDurationList[0]
|
|
ExtractedURL = ExtractedURLTitleAndDurationList[1]
|
|
ExtractedAudioURL = ExtractedURLTitleAndDurationList[2]
|
|
ExtractedAudioURL = DoubleQuoteString(ExtractedAudioURL)
|
|
MediaDuration = ExtractedURLTitleAndDurationList[3]
|
|
MediaDurationInSeconds = ConvertDurationIntoSeconds(MediaDuration)
|
|
else:
|
|
print(PrintErrorMessageIfFails)
|
|
return 'failed'
|
|
elif LenOfExtractedURLTitleAndDurationList == 3:
|
|
""" Test if both start with same protocol like http. Than
|
|
both are media urls (one is video source and other is audio
|
|
source), otherwise other one is MediaDuration.
|
|
"""
|
|
if ExtractedURLTitleAndDurationList[2].find(
|
|
ExtractedURLTitleAndDurationList[1][:4]) != -1:
|
|
|
|
MediaTitle = ExtractedURLTitleAndDurationList[0]
|
|
ExtractedURL = ExtractedURLTitleAndDurationList[1]
|
|
ExtractedAudioURL = ExtractedURLTitleAndDurationList[2]
|
|
ExtractedAudioURL = DoubleQuoteString(ExtractedAudioURL)
|
|
else:
|
|
MediaTitle = ExtractedURLTitleAndDurationList[0]
|
|
ExtractedURL = ExtractedURLTitleAndDurationList[1]
|
|
MediaDuration = ExtractedURLTitleAndDurationList[2]
|
|
MediaDurationInSeconds = ConvertDurationIntoSeconds(
|
|
MediaDuration)
|
|
elif LenOfExtractedURLTitleAndDurationList == 2:
|
|
MediaTitle = ExtractedURLTitleAndDurationList[0]
|
|
ExtractedURL = ExtractedURLTitleAndDurationList[1]
|
|
MediaDurationInSeconds = ''
|
|
else:
|
|
print(PrintErrorMessageIfFails)
|
|
return 'failed'
|
|
M3UFileName = self.MediaDomain + ' → ' + MediaTitle + '.m3u'
|
|
for Symbols in range(len(VD['ReplaceTheseSymbols'])):
|
|
M3UFileName = M3UFileName.replace(
|
|
VD['ReplaceTheseSymbols'][Symbols],
|
|
VD['ReplaceWithSymbols'][Symbols])
|
|
AudioSourceLine = ''
|
|
if ExtractedAudioURL != None:
|
|
AudioSourceLine = '#Audio:' + ExtractedAudioURL
|
|
""" Known Issue: for now, atleast 1 ASCII character would be
|
|
required in name to work in Windows since can't get unicode
|
|
output in Windows.
|
|
"""
|
|
DirectoryAndM3UFileName = os.path.join(VD['previouslist'], M3UFileName)
|
|
WriteToM3UFile(
|
|
DirectoryAndM3UFileName,
|
|
MediaDuration = MediaDurationInSeconds,
|
|
MediaTitle = MediaTitle,
|
|
MediaLocation = ExtractedURL,
|
|
AddtionalText = AudioSourceLine,
|
|
MediaURL = self.MediaURL)
|
|
DirectoryAndM3UFileName2 = os.path.join(VD['previouslist2'], M3UFileName)
|
|
WriteToM3UFile(
|
|
DirectoryAndM3UFileName2,
|
|
MediaDuration = MediaDurationInSeconds,
|
|
MediaTitle = MediaTitle,
|
|
MediaLocation = ExtractedURL,
|
|
AddtionalText = AudioSourceLine,
|
|
MediaURL = self.MediaURL)
|
|
StartProcessing.ProcessNextURL = True
|
|
DirectoryAndM3UFileName2 = DoubleQuoteString(
|
|
DirectoryAndM3UFileName2)
|
|
# M3UFileName2 = '"' + M3UFileName + '"'
|
|
if (MediaPlayer.endswith('vlc')
|
|
or MediaPlayer.endswith('vlc.exe')):
|
|
|
|
quotedExtractedURL = DoubleQuoteString(ExtractedURL)
|
|
PlayMedia(
|
|
DirectoryAndM3UFileName2,
|
|
SeprateAudio = True,
|
|
WorkaroundForVLC = quotedExtractedURL,
|
|
VLCWorkAroundTitle = MediaTitle)
|
|
else:
|
|
PlayMedia(DirectoryAndM3UFileName2, SeprateAudio = True)
|
|
#threading.Thread(target = PlayMedia(DirectoryAndM3UFileName2)).start()
|
|
|
|
|
|
# REWRITE FUNCTION WITH BETTER CLASSES-OBJECTS UNDERSTANDING
|
|
class PlayAndSave:
|
|
def __init__(self, MediaNumber, MediaURL, MediaPreference):
|
|
self.MediaNumber = MediaNumber
|
|
self.MediaURL = MediaURL
|
|
self.MediaPreference = MediaPreference
|
|
self.FileNameExtracted = False
|
|
self.FinalFileName = False
|
|
self.AudioSource = None
|
|
self.DeleteFilesTuple = ()
|
|
self.MediaTitle = ''
|
|
self.FormatsMerged = False
|
|
self.DirectoryAndFile = ''
|
|
self.FileName = ''
|
|
self.PlayingSeprateFile = False
|
|
self.MediaDomain = ParseDomainName(MediaURL)
|
|
|
|
threading.Thread(
|
|
target = self.GetAudioUrlIfSeprateAudioSource).start()
|
|
threading.Thread(target = self.Play).start()
|
|
#threading.Thread(target = self.DeleteFiles).start()
|
|
self.Save()
|
|
|
|
def GetAudioUrlIfSeprateAudioSource(self):
|
|
IsSeprateAudioStreamCommand = ' '.join((
|
|
YoutubeDL,
|
|
"-e -g --no-playlist",
|
|
AdditionalYoutubeDLArguments,
|
|
self.MediaPreference,
|
|
self.MediaURL))
|
|
IsSeprateAudioStream = RunSubprocessAndReturnOutputString(
|
|
IsSeprateAudioStreamCommand).strip('\n').split('\n')
|
|
self.MediaTitle = DoubleQuoteString(
|
|
IsSeprateAudioStream[0]).strip('"')
|
|
if len(IsSeprateAudioStream) > 2:
|
|
print('\nSeprate Audio Source')
|
|
self.AudioSource = DoubleQuoteString(
|
|
IsSeprateAudioStream[2]) #1=vdeo 2=audio
|
|
else:
|
|
self.AudioSource = ''
|
|
|
|
def Save(self):
|
|
print('Saving media... Will play as soon as its playable')
|
|
SaveCommand = ' '.join((
|
|
YoutubeDL,
|
|
"--no-part --keep-video --no-playlist",
|
|
AdditionalYoutubeDLArguments,
|
|
self.MediaPreference,
|
|
self.MediaURL))
|
|
SaveCommand = shlex.split(SaveCommand)
|
|
SaveMedia = subprocess.Popen(
|
|
SaveCommand,
|
|
cwd = VD['offlinemedia'],
|
|
stdout = subprocess.PIPE,
|
|
universal_newlines = True)
|
|
while SaveMedia.poll() is None:
|
|
Output = SaveMedia.stdout.readline()
|
|
# Do not end with '\n' newline. Stay on same line.
|
|
print(Output.strip('\n'), end = '', flush = True)
|
|
""" Backspace/clear output to print next in same line """
|
|
print('\r', end = '')
|
|
self.ExtractFileName(Output)
|
|
StartProcessing.ProcessNextURL = True
|
|
|
|
def ExtractFileName(self, Output):
|
|
if Output.startswith('[download] Destination: '):
|
|
self.FileName = Output[24:-1]
|
|
self.AfterFileNameExtracted()
|
|
elif Output.startswith('[ffmpeg] Merging formats into "'):
|
|
self.FileName = Output[31:-2]
|
|
self.AfterFileNameExtracted(ThisIsFinalFileName = True)
|
|
self.FinalFileName = True
|
|
self.FormatsMerged = True
|
|
elif Output.endswith(' has already been downloaded and merged\n'):
|
|
self.FileName = Output[11:-40]
|
|
self.AfterFileNameExtracted(ThisIsFinalFileName = True)
|
|
self.FinalFileName = True
|
|
self.FormatsMerged = True
|
|
elif Output.endswith(' has already been downloaded\n'):
|
|
self.FileName = Output[11:-29]
|
|
self.AfterFileNameExtracted()
|
|
|
|
def AfterFileNameExtracted(self, ThisIsFinalFileName = False):
|
|
self.DirectoryAndFile = VD['offlinemedia'] + self.FileName
|
|
if ThisIsFinalFileName == False:
|
|
self.DeleteFilesTuple += self.DirectoryAndFile,
|
|
self.FileNameExtracted = True
|
|
|
|
def Play(self):
|
|
global PlayingMediaNumber, PlayingSeprateFile
|
|
while PlayingMediaNumber < self.MediaNumber:
|
|
time.sleep(1/10)
|
|
while self.AudioSource == None:
|
|
time.sleep(1/10)
|
|
while self.FileNameExtracted == False:
|
|
time.sleep(1/10)
|
|
while os.path.isfile(self.DirectoryAndFile) is False:
|
|
time.sleep(1/10)
|
|
MinimumRequiredSize = (1/2) * 1048576 # 0.5MB
|
|
while os.path.getsize(self.DirectoryAndFile) < MinimumRequiredSize:
|
|
time.sleep(1/10)
|
|
threading.Thread(target = self.CreateM3UFile).start()
|
|
DirectoryAndFile = DoubleQuoteString(self.DirectoryAndFile)
|
|
if len(self.AudioSource) > 0 and self.FormatsMerged == False:
|
|
PlayingSeprateFile = True
|
|
PlayMedia(
|
|
DirectoryAndFile,
|
|
SeprateAudio = True,
|
|
AudioSource = self.AudioSource)
|
|
PlayingSeprateFile = False
|
|
else:
|
|
PlayMedia(DirectoryAndFile)
|
|
PlayingMediaNumber += 1
|
|
""" This is temporary until fix PlayingSeprateFile variable
|
|
issue in DeleteFiles()
|
|
"""
|
|
self.DeleteFiles()
|
|
|
|
def DeleteFiles(self):
|
|
while self.AudioSource == None:
|
|
time.sleep(1)
|
|
if len(self.AudioSource) == 0:
|
|
return
|
|
while self.FormatsMerged == False:
|
|
time.sleep(1)
|
|
while os.path.isfile(self.DirectoryAndFile) == False:
|
|
time.sleep(1)
|
|
# Do not delete if file is still being merged i.e. increasing size
|
|
while True:
|
|
a = os.path.getsize(self.DirectoryAndFile)
|
|
time.sleep(5)
|
|
b = os.path.getsize(self.DirectoryAndFile)
|
|
if a == b:
|
|
break
|
|
while self.PlayingSeprateFile == True: #why this not working?
|
|
#print(self.PlayingSeprateFile)
|
|
time.sleep(1)
|
|
for File in range(len(self.DeleteFilesTuple)):
|
|
FileToBeDeleted = self.DeleteFilesTuple[File]
|
|
print(
|
|
'Files are Merged, Deleting Seprate File '
|
|
+ FileToBeDeleted)
|
|
os.remove(FileToBeDeleted)
|
|
|
|
def CreateM3UFile(self):
|
|
if len(self.AudioSource) > 0:
|
|
while self.FinalFileName == False:
|
|
time.sleep(1)
|
|
M3UFileName = self.MediaDomain + ' → ' + self.MediaTitle + '.m3u'
|
|
DirectoryAndM3UFileName = os.path.join(VD['previouslist'], M3UFileName)
|
|
SavingMediaPath = RelativeIfProgrammeSubpathElseAbsolutePath(
|
|
self.DirectoryAndFile,
|
|
RelativeTo = VD['previouslist'],
|
|
DirectoryOrFile = 'file')
|
|
WriteToM3UFile(
|
|
DirectoryAndM3UFileName,
|
|
MediaLocation = SavingMediaPath,
|
|
MediaURL = self.MediaURL)
|
|
|
|
|
|
def DownloadOnly(MediaURL, MediaPreference):
|
|
FileName = ''
|
|
MediaDomain = ParseDomainName(MediaURL)
|
|
print('Downloading media. Will auto-exit once download completed.')
|
|
DownloadCommand = ' '.join((
|
|
YoutubeDL,
|
|
"--no-playlist",
|
|
AdditionalYoutubeDLArguments,
|
|
MediaPreference,
|
|
MediaURL))
|
|
DownloadCommand = shlex.split(DownloadCommand)
|
|
DownloadMedia = subprocess.Popen(
|
|
DownloadCommand,
|
|
cwd = VD['offlinemedia'],
|
|
stdout = subprocess.PIPE,
|
|
universal_newlines = True)
|
|
while DownloadMedia.poll() is None:
|
|
Output = DownloadMedia.stdout.readline()
|
|
print(Output.strip('\n'), end = '', flush = True) #do not end with '\n'
|
|
print('\r', end = '') #backspace/clear output to print next in same line
|
|
if Output.startswith('[download] Destination: '):
|
|
FileName = Output[24:-1]
|
|
elif Output.startswith('[ffmpeg] Merging formats into "'):
|
|
FileName = Output[31:-2]
|
|
elif Output.endswith(' has already been downloaded and merged\n'):
|
|
FileName = Output[11:-40]
|
|
elif Output.endswith(' has already been downloaded\n'):
|
|
FileName = Output[11:-29]
|
|
MediaTitleUpto = FileName.rfind('-')
|
|
MediaTitle = FileName[:MediaTitleUpto]
|
|
DirectoryAndFile = os.path.join(VD['offlinemedia'], FileName)
|
|
M3UFileName = MediaDomain + ' → ' + MediaTitle + '.m3u'
|
|
DirectoryAndM3UFileName = os.path.join(VD['previouslist'], M3UFileName)
|
|
DownloadedMediaPath = RelativeIfProgrammeSubpathElseAbsolutePath(
|
|
DirectoryAndFile,
|
|
RelativeTo = VD['previouslist'],
|
|
DirectoryOrFile = 'file')
|
|
WriteToM3UFile(
|
|
DirectoryAndM3UFileName,
|
|
MediaLocation = DownloadedMediaPath,
|
|
MediaURL = MediaURL)
|
|
print('Download complete')
|
|
|
|
def WatchLater(MediaURL):
|
|
MediaDomain = ParseDomainName(MediaURL)
|
|
ExtractTitleCommand = ' '.join((
|
|
YoutubeDL,
|
|
"--get-title --no-playlist",
|
|
AdditionalYoutubeDLArguments,
|
|
MediaURL))
|
|
ExtractedTitle = RunSubprocessAndReturnOutputString(
|
|
ExtractTitleCommand).strip('\n')
|
|
if len(ExtractedTitle) > 0:
|
|
MediaTitle = ExtractedTitle
|
|
M3UFileName = MediaDomain + ' → ' + MediaTitle + '.m3u'
|
|
else:
|
|
MediaTitle = MediaURL
|
|
M3UFileName = MediaTitle+'.m3u'
|
|
for Symbols in range(len(VD['ReplaceTheseSymbols'])):
|
|
M3UFileName = M3UFileName.replace(
|
|
VD['ReplaceTheseSymbols'][Symbols],
|
|
VD['ReplaceWithSymbols'][Symbols])
|
|
DirectoryAndM3UFileName = os.path.join(VD['watchlaterlist'], M3UFileName)
|
|
WatchLaterComment = "#You have marked this media to watch later"
|
|
WriteToM3UFile(
|
|
DirectoryAndM3UFileName,
|
|
MediaTitle = MediaTitle,
|
|
MediaLocation = WatchLaterComment,
|
|
MediaURL = MediaURL)
|
|
DirectoryAndM3UFileName2 = VD['watchlaterlist2'] + M3UFileName
|
|
WriteToM3UFile(
|
|
DirectoryAndM3UFileName2,
|
|
MediaTitle = MediaTitle,
|
|
MediaLocation = WatchLaterComment,
|
|
MediaURL = MediaURL)
|
|
print('Saved url to watch later list')
|
|
|
|
def WriteToM3UFile(
|
|
FilePathAndName,
|
|
MediaDuration = '',
|
|
MediaTitle = '',
|
|
MediaLocation = '',
|
|
AddtionalText = '',
|
|
MediaURL = ''):
|
|
|
|
if len(MediaTitle) > 0:
|
|
Start = '#EXTINF:'
|
|
LineBreak = '\n'
|
|
else:
|
|
Start = ''
|
|
LineBreak = ''
|
|
if len(MediaDuration) > 0:
|
|
MediaDuration = MediaDuration + ','
|
|
if len(AddtionalText) > 0:
|
|
AddtionalText = AddtionalText + '\n'
|
|
WriteContent = (
|
|
Start
|
|
+ MediaDuration
|
|
+ MediaTitle
|
|
+ LineBreak
|
|
+ MediaLocation
|
|
+ '\n'
|
|
+ AddtionalText
|
|
+ '#Source: '
|
|
+ MediaURL)
|
|
WriteGeneralTextFiles(WriteContent, FilePathAndName)
|
|
|
|
def RelativeIfProgrammeSubpathElseAbsolutePath(
|
|
Path,
|
|
RelativeTo = None,
|
|
DirectoryOrFile = 'Directory'):
|
|
|
|
Path = os.path.realpath(Path)
|
|
if Path.startswith(ProgrammeDirectory) == False:
|
|
return Path
|
|
if RelativeTo == None:
|
|
RelativeTo = ProgrammeDirectory
|
|
RelativePath = os.path.relpath(Path, RelativeTo)
|
|
return RelativePath
|
|
|
|
def PlayMedia(
|
|
FileLocation,
|
|
SeprateAudio = False,
|
|
AudioSource = None,
|
|
WorkaroundForVLC = None,
|
|
VLCWorkAroundTitle = None):
|
|
|
|
# THIS FUNCTION IS A MESS, FIX IT.
|
|
if SeprateAudio == True and AudioSource == None:
|
|
M3UFileText = ReadlinesTextFile(FileLocation)
|
|
AudioSource = GetTextAfter('#Audio:', M3UFileText).strip(' ')
|
|
QuotedMediaPlayer = DoubleQuoteString(MediaPlayer)
|
|
PlayCommand = (MediaPlayerOptions + ' ' + FileLocation).strip(' ')
|
|
if SeprateAudio == True and AudioSource != '':
|
|
MultipleSourcePlayCommand = \
|
|
MediaPlayerMultipleSourcePlayCommand
|
|
if len(MultipleSourcePlayCommand) > 0:
|
|
if WorkaroundForVLC != None:
|
|
MultipleSourcePlayCommand = \
|
|
MultipleSourcePlayCommand.replace(
|
|
'videosource', WorkaroundForVLC)
|
|
if VLCWorkAroundTitle != None:
|
|
MediaTitle = DoubleQuoteString(VLCWorkAroundTitle)
|
|
MultipleSourcePlayCommand += ' :meta-title=' + MediaTitle
|
|
else:
|
|
MultipleSourcePlayCommand = \
|
|
MultipleSourcePlayCommand.replace(
|
|
'videosource', FileLocation)
|
|
MultipleSourcePlayCommand = \
|
|
MultipleSourcePlayCommand.replace(
|
|
'audiosource', AudioSource)
|
|
PlayCommand = (
|
|
MediaPlayerOptions + ' ' + MultipleSourcePlayCommand)
|
|
else:
|
|
print(
|
|
"Error: Have to play seprate video+audio source but "
|
|
"'MediaPlayerMultipleSourcePlayCommand' is empty. "
|
|
"This is an unexpected programming bug. Please report "
|
|
"issue to IMM. Press return key to quit programme")
|
|
input()
|
|
sys.exit()
|
|
ExecuteMediaPlayerCommand = QuotedMediaPlayer + ' ' + PlayCommand
|
|
print('Now playing in media player')
|
|
#GnomeMPVExp(ExecuteMediaPlayerCommand)
|
|
#print("Gnome-MPV")
|
|
#input()
|
|
PlayingMedia = subprocess.Popen(ExecuteMediaPlayerCommand, shell = True)
|
|
""" Below method of stdout=subprocess.PIPE and readlines() is
|
|
causing perfomance issues.
|
|
"""
|
|
#PlayingMedia = subprocess.Popen(ExecuteMediaPlayerCommand, stderr = subprocess.PIPE, stdout = subprocess.PIPE, shell = True)
|
|
while PlayingMedia.poll() is None:
|
|
#PlayingMedia.stdout.readline() #Flushing by reading. Maybe otherwise output will fill the memory?
|
|
#PlayingMedia.stderr.readline()
|
|
time.sleep(1/60)
|
|
#if sys.platform == 'win32':
|
|
#subprocess.Popen(ExecuteMediaPlayerCommand, shell = True)
|
|
#else:
|
|
#os.system(ExecuteMediaPlayerCommand) #find solution to it like communicate() or wait()?
|
|
|
|
class StartProcessing:
|
|
ProcessNextURL = True
|
|
|
|
def __init__(self):
|
|
for i in range(len(Arguments)):
|
|
if Arguments[i].startswith('-'):
|
|
if Arguments[i] == '-s' or Arguments[i] == '--playandsave':
|
|
self.Do('playandsave')
|
|
return
|
|
elif Arguments[i] == '-p' or Arguments[i] == '--play':
|
|
self.Do('play')
|
|
return
|
|
elif Arguments[i] == '-d' or Arguments[i] == '--downloadonly':
|
|
self.Do('downloadonly')
|
|
return
|
|
elif Arguments[i] == '-w' or Arguments[i] == '--watchlater':
|
|
self.Do('watchlater')
|
|
return
|
|
DefaultChoice = VD['defaultchoice']
|
|
if (DefaultChoice == 'playandsave'
|
|
or DefaultChoice == 'play'
|
|
or DefaultChoice == 'downloadonly'
|
|
or DefaultChoice == 'watchlater'):
|
|
|
|
self.Do(DefaultChoice)
|
|
else:
|
|
print('Type code number of one of the options and press enter\n'
|
|
'1 - play media in desktop media player\n'
|
|
'2 - play media in desktop media player and save as well to drive\n'
|
|
'3 - only download media (don\'t play in desktop media player)\n'
|
|
'4 - save url in watch later list')
|
|
while True:
|
|
UserInput = input()
|
|
if UserInput == '1':
|
|
self.Do('play')
|
|
break
|
|
elif UserInput == '2':
|
|
self.Do('playandsave')
|
|
break
|
|
elif UserInput == '3':
|
|
self.Do('downloadonly')
|
|
break
|
|
elif UserInput == '4':
|
|
self.Do('watchlater')
|
|
break
|
|
print('Invalid option, Try Again')
|
|
|
|
""" Do what? Play, PlayAndSave, DownloadOnly, WatchLater """
|
|
def Do(self, What):
|
|
global MediaURLListString, MediaURLList, ClassMediaPreference
|
|
MediaURLList = FindMediaURLs().MediaURLs
|
|
MediaURLListString = ListIntoString(
|
|
MediaURLList, QuoteItems = 2)
|
|
if What == 'play':
|
|
ClassMediaPreference = CalculateMediaQualityPreference()
|
|
QueueInIMM = VD['queueinimm']
|
|
for i in range(len(MediaURLList)):
|
|
if QueueInIMM == 'yes':
|
|
print('Press return key to process next media url')
|
|
input()
|
|
MediaURL = MediaURLList[i]
|
|
if self.DirectlyPlayIfAvailableOffline(MediaURL) == True:
|
|
continue
|
|
MediaPreference = \
|
|
ClassMediaPreference.ManuallySelectFormat(MediaURL)
|
|
print('Processing ' + MediaURL)
|
|
threading.Thread(
|
|
target = PlayOnly,
|
|
args = (MediaURL, MediaPreference)).start()
|
|
self.WaitForProcessToComplete()
|
|
continue #is this required or will work correctly?
|
|
elif What == 'playandsave':
|
|
global PlayingMediaNumber
|
|
ClassMediaPreference = CalculateMediaQualityPreference()
|
|
PlayingMediaNumber = 0
|
|
for i in range(len(MediaURLList)):
|
|
MediaURL = MediaURLList[i]
|
|
if self.DirectlyPlayIfAvailableOffline(MediaURL) == True:
|
|
continue
|
|
MediaPreference =\
|
|
ClassMediaPreference.ManuallySelectFormat(MediaURL)
|
|
print('Processing ' + MediaURL)
|
|
threading.Thread(
|
|
target = PlayAndSave,
|
|
args = (i, MediaURL, MediaPreference)).start()
|
|
self.WaitForProcessToComplete()
|
|
continue #is this required or will work correctly?
|
|
elif What == 'downloadonly':
|
|
ClassMediaPreference = \
|
|
CalculateMediaQualityPreference(DownloadOnly = True)
|
|
ApplyORNotMediaPreference = \
|
|
VD['applymediapreferencetodownloadonlyalso']
|
|
for i in range(len(MediaURLList)):
|
|
MediaURL = MediaURLList[i]
|
|
if ApplyORNotMediaPreference == 'yes':
|
|
MediaPreference = \
|
|
ClassMediaPreference.ManuallySelectFormat(MediaURL)
|
|
if MediaPreference == 'Failed':
|
|
return
|
|
else:
|
|
MediaPreference = ''
|
|
print('Processing ' + MediaURL)
|
|
DownloadOnly(MediaURL, MediaPreference)
|
|
elif What == 'watchlater':
|
|
for i in range(len(MediaURLList)):
|
|
MediaURL = MediaURLList[i]
|
|
print('Processing ' + MediaURL)
|
|
WatchLater(MediaURL)
|
|
|
|
def WaitForProcessToComplete(self):
|
|
StartProcessing.ProcessNextURL = False
|
|
while StartProcessing.ProcessNextURL == False:
|
|
time.sleep(1)
|
|
|
|
def DirectlyPlayIfAvailableOffline(self, MediaURL):
|
|
OfflineProtocol = 'Offline://'
|
|
LenOfflineProtocol = len(OfflineProtocol)
|
|
OfflineFile = MediaURL.strip(Quotes)
|
|
if OfflineFile.startswith(OfflineProtocol):
|
|
OfflineFile = DoubleQuoteString(
|
|
OfflineFile[LenOfflineProtocol:])
|
|
PlayMedia(OfflineFile)
|
|
return True
|
|
return False
|
|
|
|
|
|
class ProgrammeArguments:
|
|
def __init__(self):
|
|
self.GetArguments()
|
|
self.SplitOneLetterArguments()
|
|
|
|
def GetArguments(self):
|
|
global Arguments
|
|
if len(sys.argv) > 1:
|
|
Arguments = sys.argv[1:]
|
|
else:
|
|
Arguments = []
|
|
|
|
""" Can below function be written in a better way? """
|
|
def SplitOneLetterArguments(self):
|
|
ArgumentsHavingValues = ['c']
|
|
ArgumentsAndValuesDictionary = {}
|
|
ItemNumberOfValue = 1
|
|
DeleteList = []
|
|
for i in range(len(Arguments)):
|
|
if Arguments[i].startswith('-') == True:
|
|
if Arguments[i].startswith('--') == False:
|
|
if len(Arguments[i]) > 2: #eg. len(-p) == 2, len(-pc) == 3
|
|
if Arguments[i].find('=') == -1: #eg. -c=xyz
|
|
SplitString = Arguments[i][1:]
|
|
for s in range(len(SplitString)):
|
|
NewItem = '-' + SplitString[s]
|
|
NewItemValue = ''
|
|
if NewItem[1:] in ArgumentsHavingValues:
|
|
NewItemValueNumber = i + ItemNumberOfValue
|
|
ItemNumberOfValue += 1
|
|
# index out of range risk
|
|
NewItemValue = Arguments[NewItemValueNumber]
|
|
DeleteList.append(NewItemValueNumber)
|
|
ArgumentsAndValuesDictionary[NewItem] = NewItemValue
|
|
DeleteList.append(i)
|
|
DeleteList = sorted(DeleteList)
|
|
for i in range(len(DeleteList)):
|
|
""" Since an i is deleted from list, next to be deleted
|
|
will be itemtobedeleted - 1. for next it will be
|
|
itemtobedeleted - 2 etc.
|
|
"""
|
|
del Arguments[DeleteList[i] - i]
|
|
for keys, values in ArgumentsAndValuesDictionary.items():
|
|
Arguments.append(keys)
|
|
if len(values) > 0:
|
|
Arguments.append(values)
|
|
|
|
|
|
class RunInTerminal:
|
|
""" Reexecute programme in terminal with received arguments if not
|
|
already running in terminal.
|
|
"""
|
|
def __init__(self):
|
|
if sys.stdout.isatty() == True:
|
|
return
|
|
else:
|
|
#find command for osx (Mac)
|
|
SupportedPlatorm = (
|
|
'linux',
|
|
'freebsd',
|
|
'openbsd',
|
|
'netbsd',
|
|
'trueos',
|
|
'darwin')
|
|
if sys.platform.startswith(SupportedPlatorm):
|
|
Arguments2 = ''
|
|
for i in range(len(Arguments)):
|
|
Argument = Arguments[i]
|
|
if Argument.startswith('-') == False:
|
|
Arguments2 += ' ' + DoubleQuoteString(Argument)
|
|
else:
|
|
Arguments2 += ' ' + Argument
|
|
Arguments2=Arguments2.replace('\'','\'\\\'\'')
|
|
""" Programme = os.path.dirname(os.path.realpath(
|
|
sys.argv[0])) + sys.argv[0][1:]
|
|
"""
|
|
Programme = sys.argv[0]
|
|
Programme = DoubleQuoteString(Programme)
|
|
ProgrammeAndArguments = SingleQuoteString(
|
|
Programme + Arguments2)
|
|
TerminalEmulator = self.FindTerminalEmulator()
|
|
if TerminalEmulator == None:
|
|
sys.exit()
|
|
ReexecuteCommand = (
|
|
TerminalEmulator + ' ' + ProgrammeAndArguments)
|
|
os.system(ReexecuteCommand)
|
|
sys.exit()
|
|
|
|
def FindTerminalEmulator(self):
|
|
SpecifiedTerminal = VD['terminalemulator']
|
|
""" Likely bug - execution command for different terminals
|
|
would be different like -e or something else. Should have a
|
|
tuple of execution command as well and return Terminal Emulator
|
|
including execution command.
|
|
"""
|
|
TerminalEmulators = (
|
|
SpecifiedTerminal,
|
|
'x-terminal-emulator',
|
|
'gnome-terminal',
|
|
'konsole',
|
|
'konsole5',
|
|
'mate-terminal',
|
|
'xfce4-terminal',
|
|
'terminal',
|
|
'qterminal',
|
|
'lxterminal',
|
|
'roxterm', 'vt',
|
|
'vte',
|
|
'vte3',
|
|
'tilix',
|
|
'terminology',
|
|
'guake',
|
|
'anyterm',
|
|
'eterm',
|
|
'yakuake',
|
|
'fbterm',
|
|
'shellinabox',
|
|
'sakura',
|
|
'xterm',
|
|
'xterm-256color',
|
|
'uxterm',
|
|
'vt52',
|
|
'vt100',
|
|
'vt102',
|
|
'vt220',
|
|
'ansi',
|
|
'dumb',
|
|
'qodem',
|
|
'termit',
|
|
'x3270-text',
|
|
'mrxvt',
|
|
'rxvt',
|
|
'x3270-x11',
|
|
'x3270',
|
|
'wterm',
|
|
'rxvt-unicode ',
|
|
'rxvt-unicode-256color',
|
|
'aterm',
|
|
'terminator',
|
|
'gtk30',
|
|
'tilda',
|
|
'yakuake',
|
|
'urxvt',
|
|
'roxterm',
|
|
'evilvte',
|
|
'sakura',
|
|
'i3',
|
|
'tmux',
|
|
'xcompmgr',
|
|
'yeahconsole',
|
|
'stjerm',
|
|
'dwm',
|
|
'sterm',
|
|
'altyo',
|
|
'mlterm',
|
|
'koi8rxterm',
|
|
'lxterm')
|
|
for i in range(len(TerminalEmulators)):
|
|
TerminalPath = FindFile(TerminalEmulators[i])
|
|
if TerminalPath != None:
|
|
TerminalPath = DoubleQuoteString(TerminalPath)
|
|
return TerminalPath + ' -e'
|
|
if sys.platform == 'darwin':
|
|
return 'terminal -e'
|
|
|
|
|
|
def GetPathsList():
|
|
FindPaths = 'echo $PATH'
|
|
PathsString = RunSubprocessAndReturnOutputString(
|
|
FindPaths, Shell = True)
|
|
PathsList = PathsString.split(Configuration.PathListSeprator)
|
|
return PathsList
|
|
|
|
def FindFile(FileNameOrPath, ReturnFullPath=True):
|
|
""" Find if specified file like media player exist in $PATH or
|
|
refering to a file that exist.
|
|
NOTE: should add current directory as well?
|
|
"""
|
|
if FileNameOrPath == None: #temporary fix
|
|
return
|
|
FileNameOrPath = FileNameOrPath.strip(Quotes)
|
|
PathsList = GetPathsList()
|
|
for i in range(len(PathsList)):
|
|
FilePath = os.path.join(PathsList[i], FileNameOrPath)
|
|
FileFound = os.path.isfile(FilePath)
|
|
if FileFound == True:
|
|
if ReturnFullPath == True:
|
|
return FilePath
|
|
return FileNameOrPath
|
|
FileFound = os.path.isfile(FileNameOrPath)
|
|
if FileFound == True:
|
|
if ReturnFullPath == True:
|
|
return os.path.abspath(FileNameOrPath)
|
|
return FileNameOrPath
|
|
|
|
def RunSubprocessAndReturnOutputString(RunCommand, Shell=False):
|
|
if Shell==False:
|
|
RunCommand=shlex.split(RunCommand)
|
|
Execute=subprocess.Popen(
|
|
RunCommand, stdout=subprocess.PIPE, shell=Shell)
|
|
Byte=Execute.stdout.read()
|
|
String=Byte.decode(Encoding)
|
|
return String
|
|
|
|
|
|
class Configuration:
|
|
""" Configure variables used in programme based on OS,
|
|
configuration file, programme arguments, working directory etc.
|
|
"""
|
|
global Quotes, PathSlash, FileProtocol
|
|
Quotes = '"\''
|
|
if sys.platform == 'win32':
|
|
PathSlash = '\\'
|
|
PathListSeprator = ';'
|
|
UserConfigurationDirectory = (
|
|
'AppData' + PathSlash + 'Roaming' + PathSlash)
|
|
else:
|
|
PathSlash = '/'
|
|
PathListSeprator = ':'
|
|
UserConfigurationDirectory = '.config' + PathSlash
|
|
|
|
FileProtocol = 'file:' + 2*PathSlash
|
|
OneDirectoryUp = '..' + PathSlash
|
|
|
|
def __init__(self):
|
|
global Encoding
|
|
self.FindVariousDirectories()
|
|
os.chdir(ProgrammeDirectory)
|
|
self.ReadConfigurationFile()
|
|
self.GenerateVariablesDictionary()
|
|
self.GlobalVariables()
|
|
self.ModifyCheckCreateDirectories()
|
|
""" This might be the problem in windows. it maybe decoding
|
|
using codepage instead of unicode. Try as utf-8 and printing
|
|
binary. if have crazy characters or numbers than solved the
|
|
problem.
|
|
"""
|
|
Encoding = sys.stdout.encoding
|
|
|
|
def FindVariousDirectories(self):
|
|
global\
|
|
ProgrammeDirectory,\
|
|
StartedWorkingDirectory,\
|
|
HomeDirectory
|
|
ProgrammeDirectory = os.path.dirname(
|
|
os.path.realpath(sys.argv[0]))
|
|
StartedWorkingDirectory = os.getcwd()
|
|
HomeDirectory = str(pathlib.Path.home())
|
|
|
|
def ReadConfigurationFile(self):
|
|
global ConfigurationFileTextLines, ConfigurationFilePath
|
|
SpecifiedConfigurationFilePath = ''
|
|
ConfigurationFileName = "imm.conf"
|
|
for i in range(len(Arguments)):
|
|
if (Arguments[i].startswith("-c=")
|
|
or Arguments[i].startswith("--configfile=")
|
|
or Arguments[i].startswith("--configurationfilepath=")):
|
|
|
|
Arg = Arguments[i]
|
|
FilePathStartsAt = Arg.find('=')
|
|
SpecifiedConfigurationFilePath = Arg[FilePathStartsAt + 1:]
|
|
ConfigurationPaths = {
|
|
"SpecifiedConfigurationFilePath " :
|
|
SpecifiedConfigurationFilePath,
|
|
"InProgrammeDirectory" : os.path.join(
|
|
ProgrammeDirectory, ConfigurationFileName),
|
|
"InHomeDirectory" : HomeDirectory
|
|
+ PathSlash
|
|
+ Configuration.UserConfigurationDirectory
|
|
+ "imm"
|
|
+ PathSlash
|
|
+ ConfigurationFileName}
|
|
for Keys, Paths in ConfigurationPaths.items():
|
|
Path = os.path.abspath(Paths)
|
|
if os.path.isfile(Path):
|
|
ConfigurationFilePath = Path
|
|
ConfigurationFileTextLines = ReadlinesTextFile(Path)
|
|
return
|
|
DefaultConfigurationTextLines = []
|
|
ConfigurationFileTextLines = DefaultConfigurationTextLines
|
|
ConfigurationFilePath = \
|
|
ConfigurationPaths["InProgrammeDirectory"]
|
|
|
|
def EditConfigurationFile(self, Variable, Value):
|
|
global ConfigurationFileTextLines
|
|
ReplaceWith = Variable + '=' + Value
|
|
Edited = False
|
|
for Lines in range(len(ConfigurationFileTextLines)):
|
|
Line = ConfigurationFileTextLines[Lines]
|
|
VariableInLineAt = Line.find('=')
|
|
VariableInLine = Line[:VariableInLineAt].strip(' ')
|
|
if VariableInLine == Variable:
|
|
ConfigurationFileTextLines[Lines] = ReplaceWith
|
|
Edited = True
|
|
if Edited == False:
|
|
ConfigurationFileTextLines.append(ReplaceWith)
|
|
WriteGeneralTextFiles(
|
|
ConfigurationFileTextLines,
|
|
ConfigurationFilePath)
|
|
|
|
def GenerateVariablesDictionary(self):
|
|
global VD #VD is VariablesDictionary
|
|
""" These variables can be modified via arguments and
|
|
configuration file. unmodifiable variables are added/overwritten
|
|
after processing configuration file and arguments.
|
|
"""
|
|
VD = {
|
|
'mediaplayeroptions' : '',
|
|
'alternatemediaplayeroptions' : '',
|
|
"mediaplayermultiplesourcesplaycommand" : '',
|
|
"alternatemediaplayermultiplesourcesplaycommand" : '',
|
|
'queueinimm' : 'no',
|
|
'appendmedialist' : 'no',
|
|
'defaultchoice' : 'ask',
|
|
'allowseprateaudiosource' : 'yes',
|
|
# Do not use internet speed if not specified.
|
|
'internetspeed':None,
|
|
# By default accept upto 4K resolution
|
|
'mediaqualityheight':2200,
|
|
'applymediapreferencetodownloadonlyalso' : 'no',
|
|
'manuallyselectformat' : 'no',
|
|
'preferaudioonly' : 'no',
|
|
'customformatoptions' : '',
|
|
'additionalyoutube-dlarguments' : '',
|
|
'previouslist' : 'Previous',
|
|
'watchlaterlist' : 'Watch Later',
|
|
'offlinemedia' : 'Offline',
|
|
'useterminal' : '',
|
|
'youtube-dl' : './Code/youtube-dl'}
|
|
self.UpdateVariablesDictionaryFromConfigurationFile(
|
|
ConfigurationFileTextLines)
|
|
#RequiredVariables = ('mediaplayer')
|
|
ExpandOneLetterArguments = {
|
|
# 'm' -> play with alternate media player or media player
|
|
# specified in argument.
|
|
'm' : 'mediaplayer',
|
|
'a' : 'preferaudioonly'}
|
|
#Use default varibales value if value not specified in argument
|
|
DefaultArgumentVariablesValue = {
|
|
'preferaudioonly' : 'yes',
|
|
'mediaplayer' : VD.get('alternatemediaplayer')}
|
|
""" shouldn't both be considered:
|
|
--variable=value & --variable value
|
|
currently considering only former. Fix it.
|
|
if i < len(Arguments):
|
|
if (Argument[i+1].startswith('-') == False
|
|
and Argument[i+1].strip(Quotes).casefold().startswith(VD['validmediaurlstart']) == False
|
|
and (Argument[i+1].strip(Quotes).casefold().startswith(FileProtocol) == False):
|
|
|
|
value = Argument[i+1]
|
|
"""
|
|
for i in range(len(Arguments)):
|
|
if Arguments[i].startswith('-'):
|
|
Arg = Arguments[i]
|
|
Seprator = Arg.find('=')
|
|
if Seprator != -1:
|
|
Variable = Arg[:Seprator].strip('-')
|
|
if Variable in ExpandOneLetterArguments:
|
|
Variable = ExpandOneLetterArguments[Variable]
|
|
Value = Arg[Seprator + 1:]
|
|
else:
|
|
Variable = Arg.strip('-')
|
|
if Variable in ExpandOneLetterArguments:
|
|
Variable = ExpandOneLetterArguments[Variable]
|
|
if Variable in DefaultArgumentVariablesValue:
|
|
Value = DefaultArgumentVariablesValue[Variable]
|
|
else:
|
|
Value = 'yes'
|
|
VD[Variable] = Value
|
|
UnmodifiableVD = {
|
|
# Should instead do if str.lowercase() == VD[key][i]:
|
|
'validmediaurlstart' : ("http://", "https://"),
|
|
'validextensioncontaningmediaurl1' : (".m3u", ".m3u8"),
|
|
# Find and replace possible conlicting symbols, atleast for
|
|
# now, later either use youtube-dl without shell = True or
|
|
# specify ASCII only characters in filenames to youtube-dl.
|
|
# remove this when fix unicode issue fixed in windows but
|
|
# if os=win32, specify *safe* characters only in youtube-dl
|
|
'ReplaceTheseSymbols' : ('?', ' : ', '"', '/', '\\', '*', '<', '>', '|'),
|
|
'ReplaceWithSymbols' : ('¿', ';', '\'', '-', '-', '★', '{', '}', 'I')}
|
|
VD.update(UnmodifiableVD)
|
|
|
|
def UpdateVariablesDictionaryFromConfigurationFile(
|
|
self, ConfigurationFileTextLines):
|
|
|
|
VariableValueSeprator = '='
|
|
for Lines in range(len(ConfigurationFileTextLines)):
|
|
Line = ConfigurationFileTextLines[Lines]
|
|
if len(Line) == 0 or Line.startswith('#') or Line.startswith('['):
|
|
continue
|
|
SepratorAt = Line.find(VariableValueSeprator)
|
|
if SepratorAt != -1:
|
|
VariableName = Line[:SepratorAt].strip(' ')
|
|
VariableName = StandardVariableName(VariableName)
|
|
VariableValue = Line[SepratorAt+1:].strip(' ')
|
|
if len(VariableName) > 0 and len(VariableValue) > 0:
|
|
VD[VariableName] = VariableValue
|
|
|
|
def GlobalVariables(self):
|
|
""" Some global vairbales from variablesdictionary for
|
|
neatness.
|
|
"""
|
|
global AdditionalYoutubeDLArguments
|
|
AdditionalYoutubeDLArguments = \
|
|
VD['additionalyoutube-dlarguments']
|
|
|
|
def ModifyCheckCreateDirectories(self):
|
|
self.ChangePathSlashesIfWindowsOS()
|
|
self.SetM3UFileAndOfflineMediaDirectory()
|
|
Directories = (
|
|
'offlinemedia',
|
|
'previouslist',
|
|
'watchlaterlist',
|
|
'previouslist2',
|
|
'watchlaterlist2')
|
|
for i in range(len(Directories)):
|
|
VD[Directories[i]] =(
|
|
os.path.abspath(VD[Directories[i]]) + PathSlash)
|
|
DirectoryUse = (
|
|
'save media file(s) to this directory',
|
|
'save media history to this directory',
|
|
'save watch later list to this directory',
|
|
'save media history to this directory',
|
|
'save watch later list to this directory')
|
|
self.CreateDirectories(
|
|
Directories, DirectoryUse)
|
|
|
|
def ChangePathSlashesIfWindowsOS(self):
|
|
if sys.platform == 'win32':
|
|
ModifyDirectories = (
|
|
'previouslist',
|
|
'watchlaterlist',
|
|
'offlinemedia')
|
|
for i in range(len(ModifyDirectories)):
|
|
VD[ModifyDirectories[i]] = (
|
|
VD[ModifyDirectories[i]].replace('/', '\\'))
|
|
|
|
def SetM3UFileAndOfflineMediaDirectory(self):
|
|
""" Use StartedWorkingDirectory for saving/downloading media
|
|
execept when same as HomeDirectory or sub directory of
|
|
ProgrammeDirectory than use ProgrammeDirectory.
|
|
"""
|
|
if (StartedWorkingDirectory.startswith(ProgrammeDirectory)
|
|
or StartedWorkingDirectory == HomeDirectory):
|
|
|
|
VD['previouslist2'] = VD['previouslist']
|
|
VD['watchlaterlist2'] = VD['watchlaterlist']
|
|
else:
|
|
SetToStartedWorkingDirectory = (
|
|
'previouslist2',
|
|
'watchlaterlist2',
|
|
'offlinemedia')
|
|
for i in range(len(SetToStartedWorkingDirectory)):
|
|
VD[SetToStartedWorkingDirectory[i]] = (
|
|
StartedWorkingDirectory)
|
|
|
|
def CreateDirectories(
|
|
self, Directories, DirectoryUse = ('unknown')):
|
|
|
|
""" Directories which are in VD only. Not a generic function
|
|
but specific
|
|
."""
|
|
for i in range(len(Directories)):
|
|
if os.path.isdir(VD[Directories[i]]) == False:
|
|
NewDirectory = VD[Directories[i]]
|
|
if NewDirectory.startswith(
|
|
ProgrammeDirectory) == True:
|
|
os.mkdir(NewDirectory)
|
|
else:
|
|
print(
|
|
Directories[i]
|
|
+ ' directory "'
|
|
+ NewDirectory
|
|
+ '" does not exist. Type \'yes\' to create\
|
|
this directory and '
|
|
+ DirectoryUse[i]
|
|
+ '. Or simply press return key to exit.')
|
|
while True:
|
|
CreateOrNot = input()
|
|
if CreateOrNot == 'yes':
|
|
try:
|
|
os.mkdir(NewDirectory)
|
|
break
|
|
except:
|
|
print(
|
|
"Could not create directory. "
|
|
"Probably permission error. Still "
|
|
"do not run as root/sudo/"
|
|
"administrator. Instead try some "
|
|
"other directory. Press return to "
|
|
"exit.")
|
|
input()
|
|
sys.exit()
|
|
if CreateOrNot == '':
|
|
print('Goodbye!')
|
|
time.sleep(3)
|
|
sys.exit()
|
|
else:
|
|
print(
|
|
"Invalid input. Type either \'yes\' "
|
|
"or nothing.")
|
|
|
|
def MediaPlayerVariables(self):
|
|
global\
|
|
MediaPlayer,\
|
|
MediaPlayerOptions,\
|
|
MediaPlayerMultipleSourcePlayCommand
|
|
if '--alternatemediaplayer' in Arguments or '-m' in Arguments:
|
|
MediaPlayer = LookupInVD('alternatemediaplayer')
|
|
if MediaPlayer != None:
|
|
MediaPlayerOptions = VD['alternatemediaplayeroptions']
|
|
MediaPlayerMultipleSourcePlayCommand = \
|
|
VD["alternatemediaplayermultiplesourcesplaycommand"]
|
|
return
|
|
else:
|
|
print(
|
|
"Alternate media player not found in configuration file"
|
|
" or arguments. Trying to use primary media player.")
|
|
MediaPlayer = self.LookupInVD("mediaplayer")
|
|
if MediaPlayer != None:
|
|
MediaPlayerOptions = VD["mediaplayeroptions"]
|
|
MediaPlayerMultipleSourcePlayCommand = \
|
|
VD["mediaplayermultiplesourcesplaycommand"]
|
|
else:
|
|
MediaPlayer = self.FindExecutable("Media Player")
|
|
MediaPlayerOptions = ''
|
|
self.EditConfigurationFile(
|
|
"mediaplayeroptions", MediaPlayerOptions)
|
|
MediaPlayerMultipleSourcePlayCommand =\
|
|
self.LookInPreConfiguredCmdOfMediaPLayers()
|
|
self.EditConfigurationFile(
|
|
"mediaplayermultiplesourcesplaycommand",
|
|
MediaPlayerMultipleSourcePlayCommand)
|
|
|
|
def LookInPreConfiguredCmdOfMediaPLayers(self):
|
|
CheckMediaPlayer = MediaPlayer
|
|
PreConfiguredCmd = {
|
|
"vlc" : "videosource :input-slave=audiosource",
|
|
"mpv" : "videosource --audio-file=audiosource"}
|
|
#Execeptions = ("gnome-mpv", "umpv")
|
|
DoesNotSupportWarning = (
|
|
"Media player may not support seprate audio source "
|
|
"playback. Seprate video+audio formats will not be "
|
|
"considered. If you know that specified media player "
|
|
"supports seprate audio source than add that coommand to "
|
|
"imm.conf file as:\n"
|
|
"\n"
|
|
"mediaplayermultiplesourcesplaycommand="
|
|
"videosource --audio audiosource\n"
|
|
"\n"
|
|
"videosource and audiosource will be replaced with "
|
|
"video url/file and audio url/file respectively.")
|
|
CheckMediaPlayer = os.path.split(MediaPlayer)[1]
|
|
if CheckMediaPlayer.endswith(".exe"):
|
|
CheckMediaPlayer = CheckMediaPlayer[:-4]
|
|
MediaPlayerCmd = PreConfiguredCmd.get(CheckMediaPlayer)
|
|
if MediaPlayerCmd == None:
|
|
print(DoesNotSupportWarning)
|
|
return ''
|
|
else:
|
|
return MediaPlayerCmd
|
|
#for Player, Cmd in PreConfiguredCmd.items():
|
|
#if CheckMediaPlayer.endswith(Player):
|
|
#if MediaPlayer.endswith(Execeptions):
|
|
#print(DoesNotSupportWarning)
|
|
#return ''
|
|
#else:
|
|
#return Cmd
|
|
#else:
|
|
#print(DoesNotSupportWarning)
|
|
#return ''
|
|
|
|
def FindExecutable(self, ExecName):
|
|
SampleExec = StandardVariableName(ExecName)
|
|
Executable = self.LookupInVD(SampleExec)
|
|
if Executable != None:
|
|
return Executable
|
|
print(
|
|
ExecName + " not found in configuration nor specified in "
|
|
"arguments. Enter " + ExecName + " run command (if any) "
|
|
"like '" + SampleExec + "' or full path to executable like"
|
|
" \"/usr/bin/" + SampleExec + "\" in GNU/Linux and other "
|
|
"unix-like OS or \"c:\\Programme files\\" + ExecName + "\\"
|
|
+ SampleExec + ".exe\" in Windows")
|
|
while True:
|
|
Executable = input()
|
|
Executable = FindFile(Executable)
|
|
if Executable == None:
|
|
print(
|
|
ExecName + " not found, try giving full path or "
|
|
"try again")
|
|
else:
|
|
self.EditConfigurationFile(SampleExec, Executable)
|
|
break
|
|
return Executable
|
|
|
|
def LookupInVD(self, VDKey):
|
|
Executable = VD.get(VDKey)
|
|
return FindFile(Executable) #return None if not found
|
|
|
|
|
|
def Main():
|
|
global MediaPlayer, YoutubeDL
|
|
ProgrammeArguments()
|
|
Config = Configuration()
|
|
RunInTerminal()
|
|
|
|
YoutubeDL = Config.FindExecutable("Youtube-DL")
|
|
Config.MediaPlayerVariables()
|
|
StartProcessing()
|
|
|
|
def GnomeMPVExp(PlayCommand):
|
|
ConfigFile = HomeDirectory+"/.config/mpv/mpv.conf"
|
|
ConfigList = ReadlinesTextFile(ConfigFile)
|
|
ConfigListBak = tuple(ConfigList)
|
|
print(PlayCommand)
|
|
PlayCommand = shlex.split(PlayCommand)[1:]
|
|
print(PlayCommand)
|
|
input()
|
|
for i in range(len(PlayCommand)):
|
|
ConfigList.append(PlayCommand[i])
|
|
WriteGeneralTextFiles(ConfigList, ConfigFile)
|
|
time.sleep(5)
|
|
os.system("gnome-mpv")
|
|
time.sleep(50)
|
|
WriteGeneralTextFiles(ConfigListBak, ConfigFile)
|
|
|
|
|
|
|
|
#==================================
|
|
|
|
Main()
|