How NOT to do business online

I went to a site that sells wood veneer slat paneling. They make a nice range of products and seem reputable. However, on completing my order I found that my payment options were limited to

  • shoppay
  • paypal
  • gpay

I have never seen a legitimate site that would not let me pay by credit card. There was a chat option and when I clicked on that it opened a chat window where I typed a question, and was immediately told to continue the chat on whatsapp. To do that I woud have to install a QR code reader on my phone, scan the given QR code, then install an app on my phone. Then link my phone to my laptop.

Or I could continue with the whatsapp web app. When I chose that I was immediately led down the rabbit hole again where I would have to install apps on my phone. Then link my phone to my laptop.

The only phone number provided was not a toll free number.

As a first time buyer I was offered a 10% discount on my first order. They emailed me the discount code. Entering the code on the purchase page did apply the discount. The email had an option of "if you have questions just reply to this email". When I did that I got "invalid recipient" and the email was never sent.

I've bought plenty of stuff from Amazon, and even from etsy. I never had this problem. This is not the way to run an online business.

ASUS Laptops – my experience

In the summer of 2022 I bought an ASUS laptop from BestBuy. It came with an internal SSD and an empty internal bay for a 2.5" drive. I didn't install a second drive until October and when I booted up it would not recognize the added drive. When I took it back to BestBuy the tech said that the problem was that the port that the drive connected to was corroded (very odd for a new machine). Because it was under warranty I had the laptop sent back to ASUS for repair. I also sent the added drive and cable with a description of the problem.

The laptop was shipped back within four days with a note stating "the customer's problem has been resolved. I am skeptical by nature so I got the BestBuy tech to boot the laptop in the store. We were both surprised to see that the drive was not recognized. To further test, the tech swapped my drive with a new SSD. This drive as well was not recognized and after a bit of haggling they agreed to take the laptop back and refund my money. My reasoning was that the laptop had a problem that ASUS was unable to fix.

ASUS did not make a similar unit at the time (I was unwilling to try another of the same) so I waited until December 26 and found another ASUS that had what I wanted. Unfortunately it did not take long for problems to start.

One of my requirements was a back-lit keyboard. The way it is supposed to work is for the back-lighting to be lit while the keyboard, mouse, or touchpad are in use, and for about 30 seconds after. Once the lighting goes out it is supposed to stay out until user activity is detected. Frequently, after timing out, the back-lighting does not come back on. I have to press Fn+F7 to cycle to the next back-lighting level (it goes low-medium-high-off). This is a pain to have to do in the dark or in low light.

The second problem was with the USB port on the left side. I have my mouse antenna plugged in there, and I would go through periods where I would get the disconnect/reconnect sound. Running a USB logger app showed me that the port was spontaneously toggling. Putting the antenna in the right side port did not show the same problem.

Because it was under warranty I again returned it for repair. The left USB is now working properly but the back-lighting is still timing out.

And yesterday a third problem arose. Unlike most laptops that have a dedicated power port, this unit has two USB C ports, both of which can accept the power cord. It makes sense. Since USB ports are powered, why not do double duty and use them for power/charging. In a pinch I can run on battery and have the use of all ports (two USB C and two USB 3). But now one of the USB C ports is getting flaky. I keep getting a pop-up indicating that I keep switching from AC to Battery.

Fortunately my warranty has not expired so it looks like it will be taking yet another trip back for service.

If you are considering an ASUS laptop I suggest you do some research to see how they measure up to the competition. My experience has not been positive.

File Server on Raspberry Pi

Having just acquired a Raspberry Pi Model 3, I am attempting to set it up as an NAS. So far I have set up the Raspberry Pi OS and configured samba. It appears to be working fine except for one minor problem. While I can remote in using TightVNC, I cannot map to any of my media folders from Windows 10 or Windows 11.

So far I have tried

  1. forcing SMB2 protocol on Pi by adding "protocol = smb2" to smb.conf
  2. enabling SMB1 in windows features

The instructions say to use "\raspberrypi\NAS" as the "browse to" folder. NAS is the name I set up in smb.conf. It asks for username/password but just responds with

2023-09-29_122752.jpg

I can browse to \10.0.0.177 or \raspberrypi and it shows my NAS share, but when I try to open NAS I just get the same error. There is probably some setting that needs to be tweaked that all the help sites I've been to are omitting because they are assuming I already know. Any suggestions will be much appreciated.

I already have file and printer sharing enabled (I can map to folders on other machines).

Windows 11 password on wake

My laptop seldom leaves my house. When I am not using it I close the lid to put it into sleep mode. I also close the lid when carrying it to another room (or perhaps into the backyard hammock). This causes a minor inconvenience as every time I open the lid I have to enter my password to unlock it. On my old (Windows 10) laptop I was able to turn this off but because Microsoft keeps changing the layout of the Settings app I had to Goggle to find out how to disable this in Windows 11. The new process was

  1. Open Settings
  2. Select Accounts
  3. Select Sign-in options
  4. Scroll down to Additional settings

Under Additional settings there is a section with the text If you've been away when should Windows require you to sign in again?" To the right of this is a drop-down box with the default text Every Time. According to Google (and Microsoft) I simply have to select a different option, like Never**.

That would be the case if the entire section were not greyed out and followed by the text Windows Hello is preventing some options from being displayed.

The recommended solution from Microsoft was to

  1. Install the latest version of Windows
  2. Apply all outstanding Windows updates
  3. Run the Surface Diagnostic Toolkit
  4. Run the built-in troubleshooter
  5. Remove Windows Hello and re-install it

Since I keep my laptop current I focused on Windows Hello. Specifically, Windows Hello is used to facilitate access via PIN, facial recognition, or fingerprint. Because my laptop has a fingerprint reader I didn't want to risk breaking it by uninstalling so kept looking.

The solution was in the command line tool, powercfg.exe. This built-in tool allows access to a wide range of setting unavailable through the Settings app. The simple fix, in my case, was to run two commands in an Admin shell.

powercfg /SETDCVALUEINDEX SCHEME_CURRENT SUB_NONE CONSOLELOCK 0
powercfg /SETACVALUEINDEX SCHEME_CURRENT SUB_NONE CONSOLELOCK 0

The first line disables automatic console locking when the machine is put into sleep mode when running off battery (DC). The second does the same when running non-battery (AC).

Now I can put my laptop to sleep and forego the password. If I want to lock and require a password I just press WINKEY+L. And I still have Windows Hello available. I have to wonder why this setting is disabled (greyed out) when it is clear that disabling the password requirement is obviously compatible with Windows Hello?

You may want to type "powercfg /?" to see the full list of options available.

Python IDE Gotcha

I came across this item while programming something that should have been very simple. VLC Media Player will save the last viewed position in a media file (assuming you have the resume option enabled). It maintains two lists, file names, and offset times. The file names are encoded such that certain characters are replaced with "%" strings (%20 for a blank, for example). I wrote a short script to remove the resume entries for files that were no longer on my computer.

The Python urllib has two handy methods for encoding/decoding these strings within URLs. In my code I had

import urllib
.
.
.
file = urllib.parse.unquote(file_list[i])

I developed the code using idlex (an idle variant). Once the code was debugged I attached it to a hotkey. Much to my surprise, running the code with the hotkey did nothing. Stripping it down to the bare essentials gave me

import urllib

print(urllib.parse.unquote('my%20file.txt'))

Running it from within idlex resulted in

======================= RESTART: D:\temp\test.py =======================
my file.txt

but running it from a cmd shell gave me

Traceback (most recent call last):
  File "D:\temp\test.py", line 3, in <module>
    print(urllib.parse.unquote('my%20file.txt'))
AttributeError: module 'urllib' has no attribute 'parse'

From the idlex shell, typing dir(urllib) gives

>>> dir(urllib)
['__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__path__', '__spec__', 'parse']

and running the same from Python invoked in a cmd shell gives

>>> dir(urllib)
['__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__path__', '__spec__']

The idlex environment adds parse. I found the same addition when I edited and ran the code in the Visual Studio Code IDE.

The solution (if you can call it that) was to code it as

from urllib import parse

print(parse.unquote('my%20file.txt'))

I have found no explanation for this behaviour, nor any reason why an IDE would provide an environment that does not accurately reflect a production environment.

Has anyone else had this Windows 11 Problem

I've only ever noticed this happening while playing a video in vlc media player. I'll have the video playing in full screen with the video's folder in the background. Without touching the mouse, keyboard, or touchpad, the background Explorer window will pop to the front, obscuring the video. I can have other apps open, but it's always an Explorer window that pops up.

Show your Python app on all Window’s desktops

I wrote a timer/alarm app in Python that I wanted to be accessible from all Windows Desktops. What I had to do until recently was run the app, select the desktops icon on my toolbar, then right click the app and select "Show this window on all desktops". I was certain there was a way I could do it from within the app and after a little research I found it. Here is a function you can call to do this. Note the usually bad practice of using an except clause to trap all possible errors.

How to enable gpedit on Windows 10 & 11

I recently fell victim to the latest Windows 11 update - KB5023706. The symptom was intermittent BSODs (blue screen of death). It wasn't until after I had done a thorough slate of hardware and system diagnostics that I came across an online tech article explaining the problem's likely origin. After I uninstalled the update and rebooted, Windows Update merrily downloaded and installed it again. Microsoft Tech Support was almost helpful in telling me to switch to a metered wifi connection and pausing updates. Their update hide/show tool refused to let me hide KB5023706 so they suggested I disable updates in the interim using the group policy editor (gpedit.msc) which they should have known has not been available on Windows Home systems for many years.

However, even though gpedit.msc does not come enabled with Windows x Home, it still actually comes bundled, but disabled. If you want to enable access to gpedit.msc, save the following script to enable-gpedit.bat and run it from an admin shell. Once you do that, and reboot, you can run it from the command line or the menu run command by typing gpedit.msc

@echo off 

pushd "%~dp0"

:: Enables gpedit.msc for Windows Home systems
::
:: Must be run from an admin shell

dir /b %SystemRoot%\servicing\Packages\Microsoft-Windows-GroupPolicy-ClientExtensions-Package~3*.mum >List.txt
dir /b %SystemRoot%\servicing\Packages\Microsoft-Windows-GroupPolicy-ClientTools-Package~3*.mum >>List.txt
for /f %%i in ('findstr /i . List.txt 2^>nul') do dism /online /norestart /add-package:"%SystemRoot%\servicing\Packages\%%i"

You can disable updates by navigating to

Local Computer Policy   
    Computer Configuration
        Administrative Templates
            Windows Components
                Windows Update
                    Manage end user experience

Double-click the Configure Automatic Updates policy on the right side.
Select the Disabled option.

Windows Update should now be paused until you manually re-enable it.

There are a ton of other settings (change at your own risk) that can otherwise only be modified by editing the registry. I've checked that this works on both Windows 10 Home and Windows 11 Home.

Programming Tools – A Rant

My older son and I are both working through online videos teaching machine learning with python. All of the materials we have seen use Jupyter Notebooks. For those of you unfamiliar with Jupyter Notebooks, they are freaking awesome. At least they are when they work. We were using two laptops because he'll be returning to San Francisco in a few days and needs to take his working environment with him. He had been using Anaconda while I was using Visual Studio Code. He discovered that cut/paste of code does not work in his Jupyter Notebook so, since he also has VS Code installed, he decided to create a notebook using that.

Problem number one - there was no option to open a new Jupyter Notebook. In fact, even when I sent him my notebook file it only opened as an XML file.

Solution number one - google the problem. Many of the solutions consisted of "make sure you have the right extensions and the right versions installed.

Problem number two - no explanation of what the correct extension versions are, how to check the current versions, or how to get and install/update the correct versions.

Solution number two - uninstall VS Code (and Visual Studio) and re-install the same version that works on my laptop.

Problem number three - every app seems to want to install its own python.exe and there were twelve (minimum) copies on his laptop, and VS Code seemed determined to use python 3.8.

Solution number three - we downloaded and installed Python 3.11. Then we had to manually edit the system PATH variable to ensure that the ...\python311 and ...\python311\scripts (for pip) were above any other entries that contained a python.exe. we were unable to reset the interpreter version for a current notebook so we had to start from scratch.

So after two hours of fighting and cursing we were finally able to get VS Code to use the 3.11 python.

Jupyter Notebooks are awesome.

When they work.

Topaz Photo AI (image recovery/enhancement)

I don't often rave about software (ok, other than my own), but I have to give two big thumbs up to the latest release by Topaz Photo AI.

I have a very large collection of family photos reaching back to the turn of the last century. I long ago converted them to digital but I have always been disappointed that so many of them were fuzzy. Through one of my many newsletters I came across the latest release of Topaz Photo AI. While there are many packages that allow tweaking of digital images, I have yet to see anything that is so easy to use (with default settings) while producing outstanding results.

The package, which retails for just under $200, includes three modules that if purchased separately would cost around $250. The modules are

  1. gigapixel (for upsizing)
  2. denoise (noise removal)
  3. enhance (facial recovery and sharpening)

The software, as downloaded, is fully functional, lacking only the ability to save the results. If you want to save your work you have to pay. Sort of. When you drop an image onto the interface and scroll via mouse-wheel until the image is fully displayed, the AI engine immediately goes to work analyzing the image. Depending on the complexity and size of the image and the capabilities of your system, the processing could take up to 30 seconds. Photo AI offloads as much processing as possible onto your GPU so a good graphics card really helps.

After the processing is complete, the displayed image is replaced by the enhanced image. Normally you would do a save at this point, but if you are happy with the result as displayed you are free to do a screen capture and save the new image that way. My screen resolution is 1920x1080 and the enhanced image is displayed in a panel approximately 1600x900. Most of my scanned photos are smaller than 1600x900 so a screen cap is easily sufficient for my needs.

Emphasis is placed on recovering faces and I have noticed that in extremely degraded images, faces are recovered quite well while clothing is not, however even with this, the resulting image is still better than the original. Automatic noise reduction is also excellent, although in some cases I still require manual touch-ups. I use FastStone Viewer (free) and I find that I can do all manual adjustments from within that application (contrast, brightness, gamma, clone/heal, etc.).

I have also found that even when taking an image which is only slightly blurry, enhancing, then flipping between the original and enhanced images demonstrates just how much nicer the slightly sharper image is.

The install will take approximately 1.2 GB of drive space.

before_1.jpg

after_1.jpg

before_2.jpg

after_2.jpg

before_3.jpg

after_3.jpg

Tech has failed us (a rant)

In the last two weeks I renewed my cable package (TV/internet/phone) and in the process got upgraded to a new, faster modem. Three months ago I bought a new ChromeCast device and was able to connect it to my old modem/network using my existing Android 6 cell phone.

Today I tried to connect my ChromeCast to the new modem. I was unable. After spending more than an hour on a chat session with google I was informed that

  1. The Google Home App (which was not visible on Google Play) no longer runs on anything older than Android 8
  2. The Windows app that used to work is no longer supported and has been yanked
  3. There is no browser app available to do the reconnect

So now, in order to use my $50 ChromeCast, I must drop $200+ on a new cell phone, or even more on a tablet.

Before you ask, my phone is not upgradable to a newer Android version. I tried to create a USB boot stick with Android 9 but just ran up against Windows Security informing me that the device was not secure. I tried to build a VMWare Android machine but it would not use my laptop's Bluetooth.

Wasn't tech supposed to make our lives better with easy interconnectivity?

Windows commenting tool in python/wxpython

Adding a file/folder comment capability to Windows

This is something I wrote a few years ago in vb.Net and just recently ported to python/wxpython. As I keep discovering, just about everything is easier in python.

Windows does not have a commenting facility so I decided to write something simple. NTFS supports something called Alternate Data Streams (ADS). For example, when you write text to a file named myfile.txt, the text goes where you would expect. But you can create an ADS for that file by appending a colon and a name to the file name. So you can write text to the ADS myfile.txt:myads and the text, rather than being stored in the file, is stored as a file attribute.

Technical note - the file text is also a data stream but with a name of null so any named data stream is an alternate data stream. Confused yet?

Anyway, since you can create any name for an ADS, I used the name comment. When I want to create a comment for a file or folder I just add :comment and write to that.

The commenting utility consists of four parts:

  1. The commenting code
  2. The GUI code
  3. The registry patch
  4. The IMDB code (optional)

The actual comment interface code consists of four methods

  1. hasComment(item)
  2. getComment(item)
  3. setComment(item, comment)
  4. deleteComment(item)

I don't think explanation is necessary. Here is the code for comment.py

"""                                                                             
    Name:                                                                       

        Comment.py                                                              

    Description:                                                                

        A set of methods to maintain a :comment alternate data stream in files  
        and folders. Alternate data streams are a feature of NTFS and are not    
        available on other filing systems.                                      

    Audit:                                                                      

        2020-06-29  rj  original code                                           

"""

import os


def __ads__(item):
    """Returns the name of the comment ADS for the given file or folder"""
    return item + ":comment"


def hasComment(item):
    """Returns True if the given file or folder has a comment"""
    return os.path.exists(__ads__(item))


def getComment(item):
    """Returns the comment associated with the given file or folder"""
    return open(__ads__(item), 'r').read() if os.path.exists(item + ":comment") else ""


def setComment(item, comment):
    """Sets the comment for the given file or folder"""
    if os.path.exists(item):
        open(__ads__(item), 'w').write(comment)


def deleteComment(item):
    """Deletes the comment for the given file or folder"""
    if os.path.exists(__ads__(item)):
        os.remove(__ads__(item))


if __name__ == "__main__":

    file = 'comment.py'

    if hasComment(file):
        print(file, 'has comment', getComment(file))
    else:
        print(file, 'does not have a comment')
    setComment(file, "this is a comment")

    if hasComment(file):
        print(file, 'has comment', getComment(file))
    else:
        print(file, 'does not have a comment')

    deleteComment(file)

    if hasComment(file):
        print(file, 'has comment', getComment(file))
    else:
        print(file, 'does not have a comment')

The GUI consists of a window with two panels. The upper panel contains a word-wrapped text control with a vertical scrollbar (if needed). The lower panel contains two button, Save and IMDB. The first button is self-explanatory. The second button, which you are free to remove, is a hook into the Internet Movie DataBase. If you should happen to have files or folders named for movies or TV series, clicking on the IMDB button will attempt to fetch the IMDB info for that name. More on this later. The code for Comment.pyw is as follows:

"""
Name:

    Comment.pyw

Description:

    Allows the user to add a comment to a file or a folder. The comment is saved
    in an alternate data stream (ADS) named "comment". If the file/folder name is
    a string that corresponds to an IMDB entry then clicking the IMDB button will
    attempt to fetch the IMDB data for that string.

Usage:

    comment file/folder

    Running the Comment.reg file in this folder will add a "Comment" context menu
    item to Windows Explorer. Before running comment.reg, ensure that the path names
    for pythonw.exe and Comment.pyw match your system.

    If you intend to use the IMDB facility you must first go to omdbapi.com and get
    a free access key which you must assign to the variable, KEY in getIMDB.py.

Audit:

    2022-09-12  rj  original code

"""

import os
import sys
import wx

from comment import *
from getIMDB import *


class MyFrame(wx.Frame):
    def __init__(self, *args, **kwds):
        # begin wxGlade: MyFrame.__init__
        kwds["style"] = kwds.get("style", 0) | wx.CAPTION | wx.CLIP_CHILDREN | wx.CLOSE_BOX | wx.RESIZE_BORDER | wx.SYSTEM_MENU
        wx.Frame.__init__(self, *args, **kwds)

        self.SetSize((400, 300))
        self.SetTitle(sys.argv[1])

        # Define a GUI consisting of an upper panel for the comment and a lower
        # panel with Save and IMDB buttons.

        self.panel_1 = wx.Panel(self, wx.ID_ANY)

        sizer_1 = wx.BoxSizer(wx.VERTICAL)

        style = wx.HSCROLL | wx.TE_MULTILINE | wx.TE_PROCESS_ENTER | wx.TE_PROCESS_TAB | wx.TE_WORDWRAP
        style = wx.TE_MULTILINE | wx.TE_PROCESS_ENTER | wx.TE_PROCESS_TAB | wx.TE_WORDWRAP

        self.txtComment = wx.TextCtrl(self.panel_1, wx.ID_ANY, "", style=style)
        self.txtComment.SetFont(wx.Font(12, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, 0, ""))
        sizer_1.Add(self.txtComment, 1, wx.EXPAND, 0)

        sizer_2 = wx.BoxSizer(wx.HORIZONTAL)

        self.btnSave = wx.Button(self.panel_1, wx.ID_ANY, "Save")
        self.btnSave.SetToolTip("Save comment and close")
        sizer_2.Add(self.btnSave, 0, 0, 0)

        sizer_2.Add((20, 20), 1, 0, 0)

        self.btnIMDB = wx.Button(self.panel_1, wx.ID_ANY, "IMDB")
        self.btnIMDB.SetToolTip("Fetch info from IMDB")
        sizer_2.Add(self.btnIMDB, 0, 0, 0)

        sizer_1.Add(sizer_2, 0, wx.EXPAND, 0)

        self.panel_1.SetSizer(sizer_1)

        self.Layout()

        # Restore last window size and position

        self.LoadConfig()

        # Bind event handlers

        self.Bind(wx.EVT_BUTTON, self.evt_save, self.btnSave)
        self.Bind(wx.EVT_BUTTON, self.evt_imdb, self.btnIMDB)
        self.Bind(wx.EVT_CLOSE, self.evt_close)
        self.Bind(wx.EVT_TEXT, self.evt_text, self.txtComment)

        # Get file/folder name from command line
        self.item = sys.argv[1]
        self.btnSave.Enabled = False

        # Display comment if one exists        
        if hasComment(item):
            self.txtComment.SetValue(getComment(item))

    def LoadConfig(self):
        """Load the last run user settings"""
        self.config = os.path.splitext(__file__)[0] + ".ini"       
        try:
            with open(self.config,'r') as file:
                for line in file.read().splitlines():
                    exec(line)
        except: pass

    def SaveConfig(self):
        """Save the current user settings for the next run"""

        x,y = self.GetPosition()
        w,h = self.GetSize()

        with open(self.config,'w') as file:
            file.write('self.SetPosition((%d,%d))\n' % (x, y))
            file.write('self.SetSize((%d,%d))\n' % (w, h))

    def evt_imdb(self, event):
        """Set comment to IMDB data for the current file/folder"""
        self.txtComment.SetValue(getIMDB(self.item))
        event.Skip()

    def evt_text(self, event):
        """Comment has changed - Enable Save button"""
        self.btnSave.Enabled = True
        event.Skip()

    def evt_save(self, event):
        """Update the comment for the current file/folder"""
        comment = self.txtComment.GetValue().strip()
        if comment:
            setComment(self.item, self.txtComment.GetValue())
        else:
            deleteComment(item)

        self.Destroy()
        event.Skip()

    def evt_close(self, event):
        """Delete the file/folder comment if all blank or null"""
        if not self.txtComment.GetValue().strip():
            deleteComment(item)
        self.SaveConfig()
        event.Skip()


class MyApp(wx.App):
    def OnInit(self):
        self.frame = MyFrame(None, wx.ID_ANY, "")
        self.SetTopWindow(self.frame)
        self.frame.Show()
        return True


if __name__ == "__main__":
    if len(sys.argv) <= 2:
        item = sys.argv[1]
        if os.path.exists(item):
            app = MyApp(0)
            app.MainLoop()

The registry patch adds the context menu for files and folders. The given patch is configured for my system. You will have to modify the path strings to correspond to

  1. The location of pythonw.exe

  2. The location of Comment.pyw

    REGEDIT4

    [HKEY_CLASSES_ROOT*\shell\Comment]
    @="Comment"

    [HKEY_CLASSES_ROOT*\shell\Comment\command]
    @="\"C:\Users\rjdeg\AppData\Local\Programs\Python\Python310\pythonw.exe\" \"D:\apps\Comment\Comment.pyw\" \"%1\""

    [HKEY_CLASSES_ROOT\Directory\Shell\Comment]
    @="Comment"

    [HKEY_CLASSES_ROOT\Directory\Shell\Comment\command]
    @="\"C:\Users\rjdeg\AppData\Local\Programs\Python\Python310\pythonw.exe\" \"D:\apps\Comment\Comment.pyw\" \"%1\""

Finally, the code for getIMDB.py is as follows:

"""
Name:

    getIMDB.py

Description:

    Given a string (item), getIMDB will query the internet moovie database (IMDB)
    and attempt to return the info for any item matching that string.

Usage:

    info = getIMDB(item)

    Item can be a string such as "War of the Worlds". Note that there are multiple
    movies with this name. If you use "War of the Worlds [1953]" you should get
    the correct info (but it's not perfect).

    Info is returned formatted as (for example)

        Year:       1953
        Runtime:    85 min
        Genre:      Action, Sci-Fi, Thriller
        Rating:     7.0
        Director:   Byron Haskin
        IMDB ID:    tt0046534

        H.G. Wells' classic novel is brought to life in this tale of alien invasion. The residents of a small town in California are excited when a flaming meteor lands in the hills. Their joy is tempered somewhat when they discover that it has passengers who are not very friendly.

        Gene Barry
        Ann Robinson
        Les Tremayne

Note:

    In order to use this you must get a free key from omdbapi.com and modify the
    value of KEY, replacing ######## with your assigned key.

Audit:

    2022-09-12  rj  original code
"""

import requests
import json
import os

def getKey(info, key):
    if key in info:
        return info[key]
    else:
        return 'n/a'

def getIMDB(item):

    # Replace ######## with your key assigned from omdapi.com
    URL = "http://www.omdbapi.com/?r=JSON&plot=FULL"
    KEY = "&apikey=########"

    # item can contain the year as 'name [yyyy]' or 'name (yyyy)'

    item  = item.replace('(','[').replace(')',']')
    temp  = item.split('\\')[-1]
    movie = os.path.splitext(temp)[0]
    title = movie.split('[')[0].strip()

    try:
        year = item.split('[')[1].replace(']','')
    except:
        year = ''

    req = URL + "&t=" + title + "&y=" + year + KEY
    res = requests.get(req)

    if res.status_code == 200:
        info = json.loads(res.text)
        return (
            'Year:\t' + getKey(info,'Year')[:4]       + '\n'   +
            'Runtime:\t' + getKey(info,'Runtime')    + '\n'   +
            'Genre:\t' + getKey(info,'Genre')      + '\n'   +
            'Rating:\t' + getKey(info,'imdbRating') + '\n'   +
            'Director:\t' + getKey(info,'Director')   + '\n' +
            'IMDB ID:\t' + getKey(info,'imdbID') + '\n\n' +
            getKey(info,'Plot') + '\n\n' +
            getKey(info,'Actors').replace(', ', '\n')
        )
    else:
        return None

if __name__ == '__main__':
    name = r'd:\My Videos\War of the Worlds [1953'
    imdb = getIMDB(name)
    print(imdb)

You can modify the format of the returned string as you like.

Anyone else tried the new Vivaldi browser?

After reading a few reviews I decided to try Vivaldi out. After importing my bookmarks and customizing my bookmark bar (just a little drag and drop) I was ready to go. Without trying any formal timing tests, my purely subjective impression is that things load faster. I don't have the tools or the patience to do formal testing so I'll wait for something official to be done later.

Like Brave, it is based on the Chrome engine, and as such supports extensions that can be found at the Chrome Store. My personal preferences are AdGuard AdBlocker, Google Dictionary, and HTML5 AutoPlay Disable. Settings are displayed much more conveniently than in Chrome, Brave, or Firefox. My only complaint was the way it handles pinch-zoom. On Brave, pinch-zoom works like most photo display apps. The whole display is zoomed with edges moving off the page, available by panning. On Vivaldi everything is made bigger and reformatted to fit on the page. Upon checking the Vivaldi forum I discovered that by going to

  • Settings
  • WebPages
  • Default Web Page Zoom

and unselecting Use CTRL-Scroll to zoom page, pinch-zoom now worked like I wanted. My initial impression is very favourable.

It's available for Windows, Linux, Mac, and Android.

wxPython Image Viewer Shell Extension

Introduction

I recently bought a new laptop. Since I haven't used VB.Net in years (nothing but Python now) I did not install Visual Studio. That meant rewriting all my VB apps in Python/wxPython. One of my most often used apps was a shell extension I wrote to add a folder right-click option to launch a small image viewer. This short tutorial will explain how the pieces work. I will not get into how to write an app in wxPython. You will find other tutorials on that here on Daniweb.

I'll post the entire project as three files

  1. ZoomPic.pyw (main code)
  2. ZoomPic.reg (Windows registry mod - need to be edited)
  3. perCeivedType.py (Utility method for file screening)

I'll check this thread periodically for questions. I likely glossed over some things that may need further explanation. There's a reason I don't get paid to do this.

Drawing the display

The images are displayed in one of two modes, windowed, or full-screen. Normally, if you wanted to display an image you would use a wx.StaticBitmap control. The process would be

  • Read the data from the file into a wx.Image object
  • Scale the image to fit the size of the display control
  • Convert the image to a bitmap and copy to the wx.StaticBitmap

There are a few problems with this approach.

  • The image will be distorted unless the container has the same aspect ratio as the image
  • You will usually get an annoying flicker when changing files

We'll handle the first problem by writing a rescale method to ensure that the image will fit in the container with no distortion. We'll handle the second by using a method called double buffering to do the redrawing of the display.

Here is the rescale method.

cw,ch   the height and width of the container
iw,ih   the height and width of the image
aspect  the aspect ratio h/w of the original image
nw,nh   the new width and height adjusting for the container

After we calculate the new width and height based on the width of the container we then check to see if the new height is bigger than the container height. If so we recalculate the new width and height based on the container height.

Note that this only creates a scaled image. It does not display it.

def ScaleToFit(self, image):
    """Scale an image to fit parent control"""

    # Get image size, container size, and calculate aspect ratio
    cw, ch = self.Size
    iw, ih = image.GetSize()
    aspect = ih / iw

    # Determine new size with aspect and adjust if any cropping
    nw = cw
    nh = int(nw * aspect)

    if nh > ch:
        nh = ch
        nw = int(nh / aspect)

    # Return the newly scaled image
    return image.Scale(int(nw*self.zoom), int(nh*self.zoom))

Now comes the part I still find confusing - direct draw using double buffering. Normally you let wxPython handle refreshing the display but in our case we are going to force a redraw on several events. We'll get to those events in a minute. For now, let's see how to do a direct draw.

We are going to create something called a device context. Consider it like a printer driver, but for the screen. You can only create a device context within the event handler for an EVT_PAINT event which we will trigger manually by calling the Refresh() method. If it sounds confusing, it is. But it will make more sense in a minute. We are going to use double buffering.

I'm going to use an analogy to explain what double buffering is. I'm either going to insult your intelligence, or expose my ignorance (based on my limited understanding). You've all seen the flip-style animation where you draw a separate picture on the sheets of a book. When you flip the pages you see an animated scene. Imagine that every time you flipped to a new page you had to wait for the artist to draw the image. That would introduce an annoying delay. Now imagine that while you are looking at one image the artist is busy rendering the next (he's really fast) so that when you flip to the next page it is already rendered.

That is a simple-minded way of looking at double buffering. The new image is not displayed until it is ready. Here's how we do it. First we create the device context. Then we clear it and draw the bitmap into the device at the given starting x and y coordinates. Then, as a bonus, we add some text to the display.

dc = wx.AutoBufferedPaintDC(self)
dc.Clear()
dc.DrawBitmap(bitmap, sx, sy, True)
dc.SetTextForeground(wx.WHITE)
dc.SetTextBackground(wx.BLACK)
dc.DrawText(self.files[self.index], 5, 5)

Note that for this to work we must have set

self.SetBackgroundStyle(wx.BG_STYLE_CUSTOM)

in the MyFrame initialization. Why this is necessary was explained to me by "you just have to". I eventually found this in the wxPython official docs.

There are a lot of methods for the device context object for drawing all sorts of shapes, icons, etc. For the first iteration of our drawing method we are going to assume an image has already been loaded from the file and that we are not concerned about zoom/pan. Here is the first iteration of the on_paint method

def on_paint(self, evt):
    """Draw the current image"""

    # Scale the image to fit and convert to bitmap           
    image  = self.ScaleToFit(self.image)
    bitmap = image.ConvertToBitmap()

    # Determine upper left corner to centre image on screen
    iw, ih = bitmap.GetSize()
    sw, sh = self.Size
    sx, sy = int((sw - iw) / 2), int((sh - ih) / 2)

    # Draw the image
    dc = wx.AutoBufferedPaintDC(self)
    dc.Clear()
    dc.DrawBitmap(bitmap, sx, sy, True)
    dc.SetTextForeground(wx.WHITE)
    dc.SetTextBackground(wx.BLACK)
    dc.DrawText(self.files[self.index], 5, 5)
    evt.Skip()

Now we want to add a few controls to the interface. I like to use hotkeys. For one, navigating with things like arrow keys is easier (for me) than constantly moving the mouse and clicking, plus, putting buttons or menus on the display takes away from the picture. The hotkeys I've defined are

arrow left/up      prev image
arrow right/down   next image
f                  toggle full-screen/windowed (later)
esc                quit  

Mouse controls are

wheel              prev/next image
left-click         zoom
left-click/drag    zoom/pan

Hotkeys (or accelerators) can be linked to menu items and buttons. Because we want the buttons to be invisible we will make them zero-sized. Then we bind them to our custom event handlers. Each event handler ends with a call to Skip() to allow wxPython to do whatever default actions it does. Our button setup looks like

# Display previous picture
self.btnPrev = wx.Button(self, wx.ID_ANY, size=(0,0))
self.btnPrev.Visible = False
self.Bind(wx.EVT_BUTTON, self.evt_prev, self.btnPrev)

# Display next picture
self.btnNext = wx.Button(self, wx.ID_ANY, size=(0,0))
self.btnNext.Visible = False
self.Bind(wx.EVT_BUTTON, self.evt_next, self.btnNext)

# Toggle fullscreen
self.btnFull = wx.Button(self, wx.ID_ANY, size=(0,0))
self.btnFull.Visible = False
self.Bind(wx.EVT_BUTTON, self.evt_full, self.btnFull)

# Exit app
self.btnExit = wx.Button(self, wx.ID_ANY, size=(0,0))
self.btnExit.Visible = False
self.Bind(wx.EVT_BUTTON, self.evt_exit, self.btnExit)

Linking the controls to hotkeys is done by

#Define hotkeys
hotkeys = [wx.AcceleratorEntry() for i in range(6)]
hotkeys[0].Set(wx.ACCEL_NORMAL, wx.WXK_DOWN, self.btnNext.Id)
hotkeys[1].Set(wx.ACCEL_NORMAL, wx.WXK_UP, self.btnPrev.Id)
hotkeys[2].Set(wx.ACCEL_NORMAL, wx.WXK_RIGHT, self.btnNext.Id)
hotkeys[3].Set(wx.ACCEL_NORMAL, wx.WXK_LEFT, self.btnPrev.Id)
hotkeys[4].Set(wx.ACCEL_NORMAL, wx.WXK_ESCAPE, self.btnExit.Id)
hotkeys[5].Set(wx.ACCEL_NORMAL, ord('f'), self.btnFull.Id)
accel = wx.AcceleratorTable(hotkeys)
self.SetAcceleratorTable(accel)

And the mouse setup looks like

# Connect Mouse events to event handlers
self.Bind(wx.EVT_LEFT_DOWN, self.on_left_down)
self.Bind(wx.EVT_LEFT_UP, self.on_left_up)
self.Bind(wx.EVT_MOTION, self.on_motion)
self.Bind(wx.EVT_MOUSEWHEEL, self.on_scroll)

The EVT_MOTION event handler will be used when zooming/panning.

Finally, we bind our handlers for painting and resizing

# Connect events to event handlers
self.Bind(wx.EVT_SIZE, self.on_size)
self.Bind(wx.EVT_PAINT, self.on_paint)
How it works

When the application starts, it scans the current working directory for image files. As a simple test, we can look for all files with an extension matching a list of known image files. That's what I use in this example in the isImage method. I'll include here a more general version which checks the Windows Registry to see what files Windows recognizes as images.

The file names are saved in a list and the first file is read into self.image. Since this is done before we bind the event handlers, when on_paint is called the first time the Window is displayed it will already have access to the first image. After that, any time we want to display a new image, all we have to do is read it into self.image then call Refresh to trigger on_paint.

By maintaining a flag (self.left_down) that keeps track of when the left mouse button is pressed, we can determine whether to display the image full size (relative to the window) or zoomed in.

One thing in particular I had trouble with was switching between windowed and full-screen. If I started the application in windowed mode I was able to toggle between modes with no problem. However, If I started in full-screen, toggling to windowed mode caused the app to hang. User Zig_Zag at the wxPython forum showed me how to fix it, but I still don't see why the fix works. Originally I had

self.ShowFullScreen(True)

in the initialization of MyFrame. When moved to OnInit for MyApp as

self.frame.ShowFullScreen(True)

the problem went away.

Adding as a shell extension

You can run ZoomPic from the command line by giving it a folder name as a parameter. More useful is having it available in the context menu for a folder. On my computer, the registry location for a folder context menu is under HKEY_CLASSES_ROOT at

Directory
    Shell
        ZoomPic
            command
                (Default) REG_SZ "C:\Users\rjdeg\AppData\Local\Programs\Python\Python310\pythonw.exe" "D:\apps\ZoomPic\ZoomPic.pyw" "%1"

There are likely other entries under Directory at the same level as Shell so we just add ZoomPic as a new one. This will not cause anything else to stop working. Note that the value of the REG_SZ value will change from system to system. You'll have to put in the full path for your pythonw.exe and zoompic.pyw. To make it easier, just take the attached file, ZoomPic.reg, modify it for your system, then run it (or double click on it) to add it to your registry.

More general file types
"""                                                                                    Name:                                                                                                                                                             
Name:

    perceivedType.py                                                            

Description:                                                                    

    This is a set of methods that use the Windows registry to return a string   
    describing how Windows interprets the given file. The current methods will  
    return a string description as provided by Windows, or "unknown" if Windows 
    does not have an associated file type. The auxiliary functions return True  
    or False for tests for specific file types.                                 

 Auxiliary Functions:                                                           

      isVideo(file) - returns True if PerceivedType = "video"               
      isAudio(file) - returns True if PerceivedType = "audio"               
      isImage(file) - returns True if PerceivedType = "image"               
      isText (file) - returns True if PerceivedType = "text"                

Parameters:                                                                     

    file:str    a file name                                                     
    degug:bool  print debug info if True (default=False)                        

Audit:                                                                          

    2021-07-17  rj  original code                                               

"""

import os
import winreg


def perceivedType(file: str, debug: bool = False) -> str:
    """Returns the windows registry perceived type string for the given file"""

    if debug:
        print(f'\nchecking {file=}')

    try:
        key = winreg.OpenKey(winreg.HKEY_CLASSES_ROOT, os.path.splitext(file)[-1])
        inf = winreg.QueryInfoKey(key)

        for i in range(0, inf[1]):
            res = winreg.EnumValue(key, i)
            if debug:
                print(f'    {res=}')
            if res[0] == 'PerceivedType':
                return res[1].lower()
    except:
        pass

    return "unknown"

def isVideo(file: str) -> str: return perceivedType(file) == 'video'
def isAudio(file: str) -> str: return perceivedType(file) == 'audio'
def isImage(file: str) -> str: return perceivedType(file) == 'image'
def isText(file: str) -> str: return perceivedType(file) == 'text'


if __name__ == '__main__':
    for file in ('file.avi', 'file.mov', 'file.txt', 'file.jpg', 'file.mp3', 'file.pdf', 'file.xyz'):
        print('Perceived type of "%s" is %s' % (file, perceivedType(file, debug=True)))
Recommended Reading

Two excellent books on developing applications in wxPython are

wxPython 2.8 Application Development Cookbook by Cody Precord.

Creating GUI Applications With wxPython by Michael Driscoll

The second book is more recent and uses wxPython features that may not have been available in earlier books.

Another excellent resource is wxPython Forum. The experts there were very helpful when I got stuck.

Problem with submitting new topics

When I create a new topic and enter the mandatory tags, the tag dialog box sometimes overlays the "Continue to the Last Step" button leaving only the merest sliver of a button to click. Could the tag dialog be aligned left to avoid this?

Windows Coordinate System Oddity

This is yet another thing that everyone just accepts, but nobody can tell me why this decision was made.

Everyone is familiar with the Windows screen coordinate system where x and y values increase to the right, for x, and down, for y. What is not noticible until you start programming window positions is that the upper left corner of the screen does not have the coordinates (0,0) as you would expect, but (-7,-7). Even more curiously, using the Windows Info tool that comes with AutoIt, if you maximize any window you will indeed see that the window is positioned at (-7,-7) but if you move the mouse cursor to the top left corner, it has coordinates (0,0).

This makes absolutely no sense to me so if anyone has a reasonable explanation I am more than willing to shout it down.

I'm also curious if this is the same on Linux. I have no easy way of checking it out. I program apps using wxPython nd one of its features is being multi-platform. If Linux doesn't use the same coordinate BS then apps would not truly be portable.

Anyone here using JellyFin Media Server?

I've seen multiple write-ups and videos on how super-easy it is to install and configure JellyFin Media Server. My experience has been the opposite. It has a lot of really nice features, one of which is the automatic fetching of meta data. It's so nice, in fact, that I thought I'd use the same web interface for remote computers on the laptop that is hosting the videos.

That's where I start having problems. Selecting a video on a client computer results in immediate playback. Selecting any video on the host computer simply gives "Playback Error - This client isn't compatible with the media and the server isn't sending a compatible media format." Googling this error results in many posts with discussions such as the following:

I got the same issue with 10.7.7 (from focal PPA) on Linux in an LXC-Container, but i think I nailed it down:

i forwarded the device-files of my nvidia p400 to the lxc-container, which is running on proxmox7, nvidia-smi runs on the host as well as on the container, so basic access to the gpu is possible, but jellyfin throws me the error noted in the title of this issue on playback attempts. It seems, that the cuda-cores did not get activated automatically. Also the /dev/nvidia-uvm does not exist on boot (inside the lxc instance).

I contend that if you need to understand the technology at that level of detail in order to troubleshoot and use JellyFin then you are not allowed to tout how "super easy" it is to use.

So is anyone out there using JellyFin, and have you had similar problems?