5959_URL_NETLOCS = {"michaeleskin.com" , "www.michaeleskin.com" }
6060
6161
62+ def _deflate (s : str , / ) -> str :
63+ """Use deflate (zlib) to compress and base64-encode `s`."""
64+ import base64
65+ import zlib
66+
67+ b = s .encode ("utf-8" )
68+ c = zlib .compress (b )
69+ b64 = base64 .b64encode (c ).decode ("ascii" )
70+ b64_for_url = b64 .replace ("+" , "-" ).replace ("/" , "_" ).rstrip ("=" )
71+ return b64_for_url
72+
73+
74+ def _inflate (s : str , / ) -> str :
75+ """Use inflate (zlib) to decompress and base64-decode `s`."""
76+ import base64
77+ import zlib
78+
79+ b64 = s .replace ("-" , "+" ).replace ("_" , "/" )
80+ pad = len (b64 ) % 4
81+ if pad == 2 :
82+ b64 += "=="
83+ elif pad == 3 :
84+ b64 += "="
85+ else :
86+ if pad != 0 :
87+ raise ValueError (f"Invalid base64 string length { len (b64 )} " )
88+ c = base64 .b64decode (b64 )
89+ b = zlib .decompress (c )
90+ return b .decode ("utf-8" )
91+
92+
6293def abctools_url_to_abc (
6394 url : str ,
6495 * ,
@@ -85,6 +116,14 @@ def abctools_url_to_abc(
85116 remove_prefs
86117 Remove lines starting with these prefixes.
87118 Use ``False`` or an empty iterable to keep all lines instead.
119+
120+ Notes
121+ -----
122+ ``def`` takes preference if both ``def`` and ``lzw`` are present in the URL query parameters.
123+
124+ See Also
125+ --------
126+ abc_to_abctools_url
88127 """
89128
90129 if not remove_prefs :
@@ -99,19 +138,36 @@ def abctools_url_to_abc(
99138 logger .debug (f"Unexpected Eskin URL path: { res .path } " )
100139
101140 query_params = parse_qs (res .query )
102- try :
103- (lzw ,) = query_params ["lzw" ]
104- except Exception as e :
105- raise ValueError ("URL does not contain required 'lzw' parameter" ) from e
106- # Note `+` has been replaced with space by parse_qs
107- # Note js LZString.compressToEncodedURIComponent() is used to compress/encode the ABC
141+ # Note `+` is now replaced with space
108142
109- try :
110- abc = LZString .decompressFromEncodedURIComponent (lzw )
111- except Exception as e :
112- raise RuntimeError ("Failed to decompress LZString data" ) from e
113- if abc is None :
114- raise RuntimeError ("Failed to decompress LZString data" )
143+ abc_params = ["def" , "lzw" ]
144+ todo = abc_params [:]
145+ while todo :
146+ param = todo .pop (0 )
147+ try :
148+ (encoded ,) = query_params [param ]
149+ except KeyError :
150+ continue
151+
152+ if param == "lzw" :
153+ try :
154+ abc = LZString .decompressFromEncodedURIComponent (encoded )
155+ except Exception as e :
156+ raise RuntimeError ("Failed to decompress LZString data" ) from e
157+ if abc is None : # pragma: no cover
158+ raise RuntimeError ("Failed to decompress LZString data" )
159+ break
160+ elif param == "def" :
161+ try :
162+ abc = _inflate (encoded )
163+ except Exception as e :
164+ raise RuntimeError ("Failed to decompress deflate data" ) from e
165+ break
166+ else : # pragma: no cover
167+ raise AssertionError (f"Unexpected ABC data parameter: { param !r} " )
168+ else :
169+ s_params = ", " .join (repr (p ) for p in abc_params )
170+ raise ValueError (f"No known ABC data parameter found in URL (tried { s_params } )" )
115171
116172 wanted_lines = [
117173 line .strip () for line in abc .splitlines () if not line .lstrip ().startswith (remove_prefs )
@@ -120,19 +176,37 @@ def abctools_url_to_abc(
120176 return "\n " .join (wanted_lines )
121177
122178
123- def abc_to_abctools_url (abc : str ) -> str :
179+ def abc_to_abctools_url (abc : str , * , lzw : bool = True ) -> str :
124180 """Create an Eskin abctools (``michaeleskin.com/abctools/``) share URL for `abc`.
125181
126182 More info: https://michaeleskin.com/tools/generate_share_link.html
183+
184+ Parameters
185+ ----------
186+ abc
187+ The tune.
188+ lzw
189+ Whether to use the original LZString compression method (``True``, default)
190+ or the newer deflate (zlib) compression method (``False``),
191+ which gives shorter URLs.
192+
193+ See Also
194+ --------
195+ abctools_url_to_abc
127196 """
128197
129198 # Must start with 'X:' (seems value is not required)
130199 if not abc .lstrip ().startswith ("X" ):
131200 abc = "X:\n " + abc
132201
133- lzw = LZString .compressToEncodedURIComponent (abc )
202+ if lzw :
203+ param = "lzw"
204+ compressed = LZString .compressToEncodedURIComponent (abc )
205+ else :
206+ param = "def"
207+ compressed = _deflate (abc )
134208
135- return f"https://michaeleskin.com/abctools/abctools.html?lzw= { lzw } "
209+ return f"https://michaeleskin.com/abctools/abctools.html?{ param } = { compressed } "
136210
137211
138212class EskinTunebookInfo (NamedTuple ):
@@ -298,8 +372,10 @@ def load_url(url: str) -> Tune:
298372 from . import load_example_abc
299373
300374 abc = load_example_abc ("For the Love of Music" )
301- url = abc_to_abctools_url (abc )
302- print (url )
375+ url_lzw = abc_to_abctools_url (abc , lzw = True )
376+ print (url_lzw )
377+ url_def = abc_to_abctools_url (abc , lzw = False )
378+ print (url_def )
303379
304380 kss = load_meta ("kss" )
305381 print (kss )
0 commit comments