Is_lossless plugin no longer working with m4a

Hello,
I start writing a full script again and I realize that the is_lossless plugin no longer correctly detects M4As.

Is_lossless
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
# -*- coding: utf-8 -*-
PLUGIN_NAME = 'Tagger script functions is_lossless() and is_lossy()'
PLUGIN_AUTHOR = 'Philipp Wolfer'
PLUGIN_DESCRIPTION = 'Tagger script functions to detect if a file is lossless or lossy'
PLUGIN_VERSION = "0.1"
PLUGIN_API_VERSIONS = ["1.3.0", "2.0", "2.1", "2.2"]
PLUGIN_LICENSE = "GPL-2.0"
PLUGIN_LICENSE_URL = "https://www.gnu.org/licenses/gpl-2.0.html"

from picard.script import register_script_function

LOSSLESS_EXTENSIONS = ['flac', 'oggflac', 'ape', 'ofr', 'tak', 'wv', 'tta', 'wav']

def is_lossless(parser):
    """
    Returns true, if the file processed is a lossless audio format.
    Note: This depends mainly on the file extension and does not look inside the
    file to detect the codec.
    """
    if parser.context['~extension'] in LOSSLESS_EXTENSIONS:
        return "1"
    elif parser.context['~extension'] == 'm4a':
        # Mutagen < 1.26 fails to detect the bitrate for Apple Lossless Audio Codec.
        if not parser.context['~bitrate'] or parser.context['~bitrate'] > 1000:
            return "1"
        else:
            return ""
    else:
        return ""

def is_lossy(parser):
    """Returns true, if the file processed is a lossy audio format."""
    if is_lossless(parser) == "1":
        return ""
    else:
        return "1"

register_script_function(is_lossless)
register_script_function(is_lossy)
Log
D: 08:04:16,896 /usr/lib/picard/picard/formats/mp4._save:249: Saving file '/media/Data0/Public/Medias/Retag3/Lossy/M4A/Arctic Monkeys/Favourite Worst Nightmare/01 Brianstorm[1411.2].m4a'
E: 08:04:16,904 /usr/lib/picard/picard/util/thread.run:64: Traceback (most recent call last):
  File "/usr/lib/picard/picard/util/thread.py", line 60, in run
    result = self.func()
  File "/usr/lib/picard/picard/file.py", line 297, in _save_and_rename
    new_filename = self._rename(old_filename, metadata)
  File "/usr/lib/picard/picard/file.py", line 453, in _rename
    self.make_filename(old_filename, metadata))
  File "/usr/lib/picard/picard/file.py", line 442, in make_filename
    new_filename = self._format_filename(new_dirname, new_filename, metadata, settings)
  File "/usr/lib/picard/picard/file.py", line 407, in _format_filename
    new_filename = self._script_to_filename(naming_format, metadata, ext, settings)
  File "/usr/lib/picard/picard/file.py", line 384, in _script_to_filename
    naming_format, metadata, file=self, settings=settings)
  File "/usr/lib/picard/picard/util/scripttofilename.py", line 59, in script_to_filename_with_metadata
    filename = ScriptParser().eval(naming_format, new_metadata, file)
  File "/usr/lib/picard/picard/script.py", line 315, in eval
    return ScriptParser._cache[key].eval(self)
  File "/usr/lib/picard/picard/script.py", line 154, in eval
    return "".join([item.eval(state) for item in self])
  File "/usr/lib/picard/picard/script.py", line 154, in <listcomp>
    return "".join([item.eval(state) for item in self])
  File "/usr/lib/picard/picard/script.py", line 145, in eval
    args = [arg.eval(parser) for arg in self.args]
  File "/usr/lib/picard/picard/script.py", line 145, in <listcomp>
    args = [arg.eval(parser) for arg in self.args]
  File "/usr/lib/picard/picard/script.py", line 154, in eval
    return "".join([item.eval(state) for item in self])
  File "/usr/lib/picard/picard/script.py", line 154, in <listcomp>
    return "".join([item.eval(state) for item in self])
  File "/usr/lib/picard/picard/script.py", line 148, in eval
    return function(parser, *args)
  File "/usr/lib/picard/picard/script.py", line 439, in func_if
    if _if.eval(parser):
  File "/usr/lib/picard/picard/script.py", line 154, in eval
    return "".join([item.eval(state) for item in self])
  File "/usr/lib/picard/picard/script.py", line 154, in <listcomp>
    return "".join([item.eval(state) for item in self])
  File "/usr/lib/picard/picard/script.py", line 148, in eval
    return function(parser, *args)
  File "/home/rickey/.config/MusicBrainz/Picard/plugins/islossless.py", line 37, in is_lossless
    if not parser.context['~bitrate'] or parser.context['~bitrate'] > 1000:
TypeError: '>' not supported between instances of 'str' and 'int'

D: 08:04:16,917 /usr/lib/picard/picard/file.update:614: Updating file <MP4File '01 Brianstorm[1411.2].m4a'>
Codec details by MediaInfo
General

Complete name : 01 Brianstorm[1411.2].m4a

Format : MPEG-4

Codec ID : M4V (isom/iso2/avc1)

File size : 21.7 MiB
Duration : 2 min 50 s
Overall bit rate mode : Variable
Overall bit rate : 1 071 kb/s
Movie name : Brianstorm

Video
ID : 1
Format : AVC
Format/Info : Advanced Video Codec
Format profile : High@L6.2
Format settings : CABAC / 4 Ref Frames
Format settings, CABAC : Yes
Format settings, ReFrames : 4 frames
Codec ID : avc1
Codec ID/Info : Advanced Video Coding
Width : 500 pixels
Height : 448 pixels
Display aspect ratio : 1.116
Frame rate mode : Constant
Frame rate : 90 000.000 FPS
Color space : YUV
Chroma subsampling : 4:2:0
Bit depth : 8 bits
Scan type : Progressive
Stream size : 0.00 Byte (0%)
Source stream size : 3.95 KiB (0%)
Writing library : x264 core 152 r2854 e9a5903
Encoding settings : cabac=1 / ref=3 / deblock=1:0:0 / analyse=0x3:0x113 / me=hex / subme=7 / psy=1 / psy_rd=1.00:0.00 / mixed_ref=1 / me_range=16 / chroma_me=1 / trellis=1 / 8x8dct=1 / cqm=0 / deadzone=21,11 / fast_pskip=1 / chroma_qp_offset=-2 / threads=6 / lookahead_threads=1 / sliced_threads=0 / nr=0 / decimate=1 / interlaced=0 / bluray_compat=0 / constrained_intra=0 / bframes=3 / b_pyramid=2 / b_adapt=1 / b_bias=0 / direct=1 / weightb=1 / open_gop=0 / weightp=2 / keyint=250 / keyint_min=25 / scenecut=40 / intra_refresh=0 / rc_lookahead=40 / rc=crf / mbtree=1 / crf=23.0 / qcomp=0.60 / qpmin=0 / qpmax=69 / qpstep=4 / ip_ratio=1.40 / aq=1:1.00
Color range : Full


Audio
ID : 2
Format : ALAC
Codec ID : alac
Codec ID/Info : Apple Lossless Audio Codec
Duration : 2 min 50 s
Duration_LastFrame : -88 ms
Bit rate mode : Variable
Bit rate : 1 068 kb/s
Nominal bit rate : 1 411 kb/s
Channel(s) : 2 channels
Sampling rate : 44.1 kHz
Bit depth : 16 bits
Stream size : 21.7 MiB (100%)
Default : Yes
Alternate group : 1

When I comment on this part of the code, the error disappears but no M4A is detected correctly of course.

elif parser.context['~extension'] == 'm4a':
        # Mutagen < 1.26 fails to detect the bitrate for Apple Lossless Audio Codec.
        if not parser.context['~bitrate'] or parser.context['~bitrate'] > 1000:
            return "1"
        else:
            return ""

what’s wrong since version 2.0?

I didn’t even remember I shared this script in the past :slight_smile:

But the version I currently use is at https://github.com/phw/picard-plugins/blob/formathelpers/plugins/formathelpers/init.py and that takes care of the bug.

Keep in mind that these functions are a bit unreliable in any case. It mainly just detects the lossless status based on file extension with some special handling for m4a files which is more a heuristic than a reliable detection of lossless codec. This works for my use case, but is also the reason why I never submitted this plugin for inclusion in the official plugin list. Basically I don’t want to support this code beyond what it is already doing and which fits my needs.

3 Likes

Thank you so much.

I know it’s not reliable, but it avoids a lot of lines of code.
Besides, I have a script that allows us to refine the results, and I plan to improve it to complement the islossless.py plugin.

Birate
$noop(################ BITRATE TYPE DETECTION ################)
$set(BRType,$if($eq_any(%_bitrate%,320.0,256.0,224.0,192.0,160.0,128.0,112.0,96.0,80.0,64.0,56.0,48.0,40.0,32.0,24.0,16.0,8.0),CBR,VBR))
$if($eq(%BRType%,VBR),$set(BrVBR,$left(%_bitrate%,3)))
$if($eq($upper(%_extension%),WMA), 
	$if($eq(%BRType%,VBR),
			$if($gte(%BrVBR%,256),$set(wmatype,VBR),
			$if($lt(%BrVBR%,192),$set(wmatype,VBR),
			$if($lt(%BrVBR%,128),$set(wmatype,VBR),
			$if($lt(%BrVBR%,96),$set(wmatype,VBR),
			$if($lt(%BrVBR%,80),$set(wmatype,VBR),
			))))))
	$if($not(%wmatype%),$set(wmatype,CBR)))
$if($eq($upper(%_extension%),WMA),$set(_BRType,%wmatype%),)
$if($not(%_BRType%),$set(_BRType,%BRType%))
$noop(################ END OF BITRATE TYPE DETECTION ################)

I noticed with MediaInfo, that some files have the “compression mode:” tag:
is it possible to search for this tag with a plugin? same thing with different tags like “Bit rate mode:” / “Overall bit rate mode:”; “Codec ID:”; etc …

I have found the plugin script at picard-plugins/__init__.py at formathelpers · phw/picard-plugins · GitHub I have found that opus format doesn’t seam to want to work for some reason. Would be great to get this into musicbrainz plugin options.

Can you explain in more detail what doesn’t work? Does it wrongly detect the opus files as lossless?

no I have this part in my script
[$if($is_lossless(),%_bits_per_sample%-bit $div(%_sample_rate%,1000) kHz %_extension%,$replace(%_bitrate%,.0,)kbps %_extension%) %media%]

for any tracks that are opus, i don’t get the kbps value at all. if i open the track in mp3tag i can see the kbps value and mp3, m4a all work fine.

That has nothing to do with the plugin, the plugin only provides the $is_lossless() used in your script.

Regarding the bitrate that’s because mutagen does not support reading the bitrate from Opus files, see oggopus: provide bitrate · Issue #475 · quodlibet/mutagen · GitHub

2 Likes

ah thank you for pointing me in the right direction

1 Like

thank you so how does the plugin work with mutagen? is mutagen built into picard?

Yes, Picard uses mutagen to read and write tags.

Thank you for your help, so just have to hope that mutagen can fix the issue and then check for update to Picard to incorporate the fixes

If anyone is still here, I fixed the plugin.

# -*- coding: utf-8 -*-
#
# Copyright (C) 2014, 2017, 2021 Philipp Wolfer
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.

PLUGIN_NAME = 'Tagger script functions $is_lossless() and $is_lossy()'
PLUGIN_AUTHOR = 'Philipp Wolfer'
PLUGIN_DESCRIPTION = 'Tagger script functions to detect if a file is lossless or lossy'
PLUGIN_VERSION = "0.2"
PLUGIN_API_VERSIONS = ["2.0", "2.1", "2.2", "2.3", "2.4", "2.5", "2.6"]
PLUGIN_LICENSE = "GPL-2.0"
PLUGIN_LICENSE_URL = "https://www.gnu.org/licenses/gpl-2.0.html"


LOSSLESS_EXTENSIONS = ['flac', 'oggflac', 'ape', 'ofr', 'tak', 'wv', 'tta',
                       'aiff', 'aif', 'wav']

from picard.script import register_script_function


def is_lossless(parser):
    """
    Returns true, if the file processed is a lossless audio format.
    Note: This depends mainly on the file extension and does not look inside the
    file to detect the codec.
    """
    if parser.context['~extension'] in LOSSLESS_EXTENSIONS:
        return "1"
    elif parser.context['~extension'] == 'm4a':
        # Mutagen < 1.26 fails to detect the bitrate for Apple Lossless Audio Codec.
        if not float(parser.context['~bitrate']) or float(parser.context['~bitrate']) > 1000:
            return "1"
        else:
            return ""
    else:
        return ""


def is_lossy(parser):
    """Returns true, if the file processed is a lossy audio format."""
    if is_lossless(parser) == "1":
        return ""
    else:
        return "1"


register_script_function(is_lossless)
register_script_function(is_lossy)