diff --git a/script.black.bars.never/LICENSE b/script.black.bars.never/LICENSE new file mode 100644 index 000000000..333c3b09d --- /dev/null +++ b/script.black.bars.never/LICENSE @@ -0,0 +1,9 @@ +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice (including the next paragraph) shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/script.black.bars.never/README.md b/script.black.bars.never/README.md new file mode 100644 index 000000000..cb706aa1a --- /dev/null +++ b/script.black.bars.never/README.md @@ -0,0 +1,54 @@ +# BlackBarsNever Kodi Addon - Remove black bars + +# How it works + +This is an addon that eliminates black bars on KODI, whether hardcoded or the video is just wide format + +With addon installed and enabled, it will automatically analyze media on playback and determine +if there are any black bars. The addon will then zoom the media exactly enough to cover the display. + +The picture will not be distorted in any way as the zoom is linear, +however, on most media, small parts on the left and right will be cut off. Luckily, everything that's +important tends to fall in the middle of the scene 99% of the time. The advantages of experiencing an +immersive picture that fills the periphery should be enough to overweigh the disdvantage of missing sides. + +# Supported platforms + +- [x] Linux +- [x] Windows +- [x] macOS and iOS +- [x] Android & Embedded Systems - with workaround method + +# Android & Embedded Systems like \*ELEC + +Currently, Kodi can't capture sreenshots in Android and Embedded Systems if hardware accelertion is enabled due to some technical limitations. This may change in future and when that happens the addon will work properly like in other platforms. For now there's two options: + +1. Disable hardware acceleration (turn off MediaCodec Surface in Android). The problem with this is that Kodi will now use CPU for decoding and playback may be affected to the point of being unwatchable, especially for high bitrate media. Also in the devices I tested, HDR won't work on Android if hardware acceleration is turned on, I am not sure if this affects all of Android. + +2. Enable the Android & Embedded Systems Workaround from the addon settings. This feature requires an internet connection to fetch media metadata, and works best if your library adopts a decent naming pattern i.e `Title Year`. Also works properly only if media aspect ratio is unchanged from original (i.e has not been cropped from the original) + +# Installation + +Download the zip file from [releases](https://github.com/osumoclement/script.black.bars.never/releases) + +Launch Kodi >> Add-ons >> Get More >> Install from zip file + +You might want to turn off Overscan if your display is a TV by going to settings-> Aspect Ratio -> Just Scan + +Feel free to ask any questions, submit feature/bug reports + +# Multiple Aspect Ratios in Media + +For media with multiple aspect ratios, the addon will notify you of this, and will do nothing. In such cases, I recommend you watch the media as is, since if you change the aspect ratio manually, you may not know where in the media the ratio changes in order to adjust again. +This feature requires internet to work + +# Customization + +There are a few ways to customize the addon +By default, the addon automatically removes black bars. If you want to change this behavior, you can turn this off in the addon settings. You would then need to manually trigger the addon by manually calling it from elsewhere in Kodi (ie from a Skin) like this `RunScript(script.black.bars.never,toggle)`. You could even map this to a key for convenience + +To check the addon status elsewhere from Kodi, use this `xbmcgui.Window(10000).getProperty('blackbarsnever_status')`. The result is either `on` or `off` + +# License + +BlackBarsNever is [GPLv3 licensed](https://github.com/osumoclement/script.black.bars.never/blob/main/LICENSE). You may use, distribute and copy it under the license terms. diff --git a/script.black.bars.never/addon.py b/script.black.bars.never/addon.py new file mode 100644 index 000000000..624da2b32 --- /dev/null +++ b/script.black.bars.never/addon.py @@ -0,0 +1,4 @@ +from resources.lib.blackbarsnever import Main + +if (__name__ == "__main__"): + Main() \ No newline at end of file diff --git a/script.black.bars.never/addon.xml b/script.black.bars.never/addon.xml new file mode 100644 index 000000000..442975419 --- /dev/null +++ b/script.black.bars.never/addon.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + executable + + + all + BlackBarsNever + This addon eliminates the problem of black bars. If the black bars are hardcoded, the addon will automatically + detect this and remove them. If the video is a wide screen format, the addon will also detect this and remove the black bars too. + + MIT + + + https://github.com/osumoclement/script.black.bars.never + cheensa.com + + + icon.png + fanart.jpeg + resources/screenshot-01.jpg + resources/screenshot-02.jpg + + Updated the addon to use new addon.xml metadata + + diff --git a/script.black.bars.never/changelog.txt b/script.black.bars.never/changelog.txt new file mode 100644 index 000000000..3c75b86c7 --- /dev/null +++ b/script.black.bars.never/changelog.txt @@ -0,0 +1 @@ +1.0.0 Initial Release \ No newline at end of file diff --git a/script.black.bars.never/fanart.jpeg b/script.black.bars.never/fanart.jpeg new file mode 100644 index 000000000..636372c4b Binary files /dev/null and b/script.black.bars.never/fanart.jpeg differ diff --git a/script.black.bars.never/icon.png b/script.black.bars.never/icon.png new file mode 100644 index 000000000..344ed5f53 Binary files /dev/null and b/script.black.bars.never/icon.png differ diff --git a/script.black.bars.never/imdb.py b/script.black.bars.never/imdb.py new file mode 100644 index 000000000..8a7509b71 --- /dev/null +++ b/script.black.bars.never/imdb.py @@ -0,0 +1,80 @@ +import requests +from bs4 import BeautifulSoup + +import xbmc +import xbmcgui + +def notify(msg): + xbmcgui.Dialog().notification("BlackBarsNever", msg, None, 1000) + +def getOriginalAspectRatio(title, imdb_number=None): + BASE_URL = "https://www.imdb.com/" + HEADERS = { + 'User-Agent': 'Mozilla/5.0 (iPad; CPU OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148'} + + if imdb_number and str(imdb_number).startswith("tt"): + URL = "{}/title/{}/".format(BASE_URL, imdb_number) + else: + URL = BASE_URL + "find/?q={}".format(title) + search_page = requests.get(URL, headers=HEADERS) + + # lxml parser would have been better but not currently supported in Kodi + soup = BeautifulSoup(search_page.text, 'html.parser') + + title_url_tag = soup.select_one( + '.ipc-metadata-list-summary-item__t') + if title_url_tag: + # we have matches, pick the first one + title_url = title_url_tag['href'] + imdb_number = title_url.rsplit( + '/title/', 1)[-1].split("/")[0] + # this below could have worked instead but for some reason SoupSieve not working inside Kodi + """title_url = soup.css.select( + '.ipc-metadata-list-summary-item__t')[0].get('href') + """ + + URL = BASE_URL + title_url + + title_page = requests.get(URL, headers=HEADERS) + soup = BeautifulSoup(title_page.text, 'html.parser') + + # this below could have worked instead but for some reason SoupSieve not working inside Kodi + aspect_ratio_tags = soup.find( + attrs={"data-testid": "title-techspec_aspectratio"}) + + if aspect_ratio_tags: + aspect_ratio_full = aspect_ratio_tags.select_one( + ".ipc-metadata-list-item__list-content-item").decode_contents() + + """aspect_ratio_full = soup.find( + attrs={"data-testid": "title-techspec_aspectratio"}).css.select(".ipc-metadata-list-item__list-content-item")[0].decode_contents() + """ + + if aspect_ratio_full: + aspect_ratio = aspect_ratio_full.split(':')[0].replace('.', '') + else: + # check if video has multiple aspect ratios + URL = "{}/title/{}/technical/".format(BASE_URL, imdb_number) + tech_specs_page = requests.get(URL, headers=HEADERS) + soup = BeautifulSoup(tech_specs_page.text, 'html.parser') + aspect_ratio_li = soup.select_one("#aspectratio").find_all("li") + if len(aspect_ratio_li) > 1: + aspect_ratios = [] + + for li in aspect_ratio_li: + aspect_ratio_full = li.select_one( + ".ipc-metadata-list-item__list-content-item").decode_contents() + + aspect_ratio = aspect_ratio_full.split(':')[0].replace('.', '') + sub_text = li.select_one(".ipc-metadata-list-item__list-content-item--subText").decode_contents() + + if sub_text == "(theatrical ratio)": + xbmc.log("using theatrical ratio " + str(aspect_ratio), level=xbmc.LOGINFO) + return aspect_ratio + + + aspect_ratios.append(aspect_ratio) + + return aspect_ratios + + return aspect_ratio \ No newline at end of file diff --git a/script.black.bars.never/resources/language/resource.language.en_gb/strings.po b/script.black.bars.never/resources/language/resource.language.en_gb/strings.po new file mode 100644 index 000000000..aeaa369d2 --- /dev/null +++ b/script.black.bars.never/resources/language/resource.language.en_gb/strings.po @@ -0,0 +1,35 @@ +# Black Bars Never language file +# Addon Name: Black Bars Never +# Addon id: script.black.bars.never +# Addon Provider: Clement Osumo + +msgid "" +msgstr "" +"Project-Id-Version: Black Bars Never\n" +"Report-Msgid-Bugs-To: Clement Osumo\n" +"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" +"PO-Revision-Date: 2023-06-18 10:08+0000\n" +"Last-Translator: Clement Osumo\n" +"Language-Team: English (United States)\n" +"Language: en_us\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.8\n" + +msgctxt "#32001" +msgid "General" +msgstr "General" + +msgctxt "#32002" +msgid "Automatically remove black bars" +msgstr "Automatically remove black bars" + +msgctxt "#32003" +msgid "Toggle status" +msgstr "Toggle status" + +msgctxt "#32004" +msgid "Workaround for Android & Embedded (requires internet)" +msgstr "Workaround for Android & Embedded (requires internet)" \ No newline at end of file diff --git a/script.black.bars.never/resources/lib/blackbarsnever.py b/script.black.bars.never/resources/lib/blackbarsnever.py new file mode 100644 index 000000000..351af7de8 --- /dev/null +++ b/script.black.bars.never/resources/lib/blackbarsnever.py @@ -0,0 +1,219 @@ +import os +import sys + +import xbmc +import xbmcaddon +import xbmcgui +import soupsieve + +from imdb import getOriginalAspectRatio + +monitor = xbmc.Monitor() +capture = xbmc.RenderCapture() +player = xbmc.Player() + +CaptureWidth = 48 +CaptureHeight = 54 + + +def notify(msg): + xbmcgui.Dialog().notification("BlackBarsNever", msg, None, 1000) + + +class Player(xbmc.Player): + def __init__(self): + xbmc.Player.__init__(self) + + if "toggle" in sys.argv: + if xbmcgui.Window(10000).getProperty("blackbarsnever_status") == "on": + self.showOriginal() + else: + self.abolishBlackBars() + + def onAVStarted(self): + if xbmcaddon.Addon().getSetting("automatically_execute") == "true": + self.abolishBlackBars() + else: + self.showOriginal() + + def CaptureFrame(self): + capture.capture(CaptureWidth, CaptureHeight) + capturedImage = capture.getImage(2000) + return capturedImage + + ############## + # + # LineColorLessThan + # _bArray: byte Array that contains the data we want to test + # _lineStart: where to start testing + # _lineCount: how many lines to test + # _threshold: value to determine testing + # returns: True False + ############### + + def LineColorLessThan(self, _bArray, _lineStart, _lineCount, _threshold): + __sliceStart = _lineStart * CaptureWidth * 4 + __sliceEnd = (_lineStart + _lineCount) * CaptureWidth * 4 + + # zero out the alpha channel + i = __sliceStart + 3 + while i < __sliceEnd: + _bArray[i] &= 0x00 + i += 4 + + __imageLine = _bArray[__sliceStart:__sliceEnd] + __result = all([v < _threshold for v in __imageLine]) + + return __result + + ############### + # + # GetAspectRatioFromFrame + # - returns Aspect ratio * 100 (i.e. 2.35 = 235) + # Detects hardcoded black bars + ############### + + def GetAspectRatioFromFrame(self): + __aspect_ratio = int((capture.getAspectRatio() + 0.005) * 100) + __threshold = 25 + + line1 = "Interim Aspect Ratio = " + str(__aspect_ratio) + xbmc.log(line1, level=xbmc.LOGINFO) + + # screen capture and test for an image that is not dark in the 2.40 + # aspect ratio area. keep on capturing images until captured image + # is not dark + while True: + __myimage = self.CaptureFrame() + + xbmc.log(line1, level=xbmc.LOGINFO) + + __middleScreenDark = self.LineColorLessThan(__myimage, 7, 2, __threshold) + if __middleScreenDark == False: + # xbmc.sleep(1000) + break + else: + pass + # xbmc.sleep(1000) + + # Capture another frame. after we have waited for transitions + # __myimage = self.CaptureFrame() + __ar185 = self.LineColorLessThan(__myimage, 0, 1, __threshold) + __ar200 = self.LineColorLessThan(__myimage, 1, 3, __threshold) + __ar235 = self.LineColorLessThan(__myimage, 1, 5, __threshold) + + if __ar235 == True: + __aspect_ratio = 235 + + elif __ar200 == True: + __aspect_ratio = 200 + + elif __ar185 == True: + __aspect_ratio = 185 + + return __aspect_ratio + + def abolishBlackBars(self): + xbmcgui.Window(10000).setProperty("blackbarsnever_status", "on") + # notify(xbmcgui.Window(10000).getProperty('blackbarsnever_status')) + + original_aspect_ratio = None + android_workaround = ( + xbmcaddon.Addon().getSetting("android_workaround") == "true" + ) + + imdb_number = xbmc.getInfoLabel("VideoPlayer.IMDBNumber") + if player.getVideoInfoTag().getMediaType() == "episode": + # media is a TV show + title = player.getVideoInfoTag().getTVShowTitle() + else: + # media is probably a film + title = player.getVideoInfoTag().getTitle() + if not title: + title = player.getVideoInfoTag().getOriginalTitle() + if not title: + title = ( + os.path.basename(player.getVideoInfoTag().getFilenameAndPath()) + .split("/")[-1] + .split(".", 1)[0] + ) + + original_aspect_ratio = getOriginalAspectRatio(title, imdb_number=imdb_number) + + if isinstance(original_aspect_ratio, list): + # media has multiple aspect ratios, show unaltered and let user do manual intervention + notify("Multiple aspect ratios detected") + else: + if android_workaround and original_aspect_ratio: + aspect_ratio = int(original_aspect_ratio) + + self.doStiaff(aspect_ratio) + else: + aspect_ratio = self.GetAspectRatioFromFrame() + self.doStiaff(aspect_ratio) + + def doStiaff(self, ratio): + aspect_ratio = ratio + aspect_ratio2 = int((capture.getAspectRatio() + 0.005) * 100) + + window_id = xbmcgui.getCurrentWindowId() + line1 = ( + "Calculated Aspect Ratio = " + + str(aspect_ratio) + + " " + + "Player Aspect Ratio = " + + str(aspect_ratio2) + ) + + xbmc.log(line1, level=xbmc.LOGINFO) + + if aspect_ratio > 178: + zoom_amount = aspect_ratio / 178 + else: + zoom_amount = 1.0 + + # zoom in a sort of animated way, isn't working for now + iterations = (zoom_amount - 1) / 0.01 + # for x in range(iterations): + if (aspect_ratio > 178) and (aspect_ratio2 == 178): + # this is 16:9 and has hard coded black bars + xbmc.executeJSONRPC( + '{"jsonrpc": "2.0", "method": "Player.SetViewMode", "params": {"viewmode": {"zoom": ' + + str(zoom_amount) + + ' }}, "id": 1}' + ) + notify("Black Bars were detected. Zoomed {:0.2f}".format(zoom_amount)) + elif aspect_ratio > 178: + # this is an aspect ratio wider than 16:9, no black bars, we assume a 16:9 (1.77:1) display + xbmc.executeJSONRPC( + '{"jsonrpc": "2.0", "method": "Player.SetViewMode", "params": {"viewmode": {"zoom": ' + + str(zoom_amount) + + ' }}, "id": 1}' + ) + if zoom_amount <= 1.02: + notify( + "Wide screen was detected. Slightly zoomed {:0.2f}".format( + zoom_amount + ) + ) + elif zoom_amount > 1.02: + notify("Wide screen was detected. Zoomed {: 0.2f}".format(zoom_amount)) + + def showOriginal(self): + xbmcgui.Window(10000).setProperty("blackbarsnever_status", "off") + # notify(xbmcgui.Window(10000).getProperty('blackbarsnever_status')) + + xbmc.executeJSONRPC( + '{"jsonrpc": "2.0", "method": "Player.SetViewMode", "params": {"viewmode": {"zoom": 1.0' + + ' }}, "id": 1}' + ) + notify("Showing original aspect ratio") + +class Main: + p = Player() + + while not monitor.abortRequested(): + # Sleep/wait for abort for 10 seconds + if monitor.waitForAbort(10): + # Abort was requested while waiting. We should exit + break diff --git a/script.black.bars.never/resources/screenshot-01.jpg b/script.black.bars.never/resources/screenshot-01.jpg new file mode 100644 index 000000000..a63143805 Binary files /dev/null and b/script.black.bars.never/resources/screenshot-01.jpg differ diff --git a/script.black.bars.never/resources/screenshot-02.jpg b/script.black.bars.never/resources/screenshot-02.jpg new file mode 100644 index 000000000..72cb9b647 Binary files /dev/null and b/script.black.bars.never/resources/screenshot-02.jpg differ diff --git a/script.black.bars.never/resources/settings.xml b/script.black.bars.never/resources/settings.xml new file mode 100644 index 000000000..27873abaf --- /dev/null +++ b/script.black.bars.never/resources/settings.xml @@ -0,0 +1,26 @@ + + +
+ + + + 0 + true + + + + 0 + RunScript(script.black.bars.never, toggle) + + true + + + + 0 + false + + + + +
+