From 727ba7da490578f477099e9f3085eb80c78e195e Mon Sep 17 00:00:00 2001 From: kjc1 Date: Fri, 1 Mar 2024 08:55:01 +0000 Subject: [PATCH 1/6] Implementation with comments --- mutagen/id3/_file.py | 18 ++++++++++++++--- tests/test_id3.py | 47 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 3 deletions(-) diff --git a/mutagen/id3/_file.py b/mutagen/id3/_file.py index 95c460f2..123957ee 100644 --- a/mutagen/id3/_file.py +++ b/mutagen/id3/_file.py @@ -18,6 +18,7 @@ BitPaddedInt from ._tags import ID3Tags, ID3Header, ID3SaveConfig from ._id3v1 import MakeID3v1, find_id3v1 +import os.path @enum @@ -221,8 +222,8 @@ def _prepare_data(self, fileobj, start, available, v2_version, v23_sep, @convert_error(IOError, error) @loadfile(writable=True, create=True) def save(self, filething=None, v1=1, v2_version=4, v23_sep='/', - padding=None): - """save(filething=None, v1=1, v2_version=4, v23_sep='/', padding=None) + padding=None, preserve_mtime=False): + """save(filething=None, v1=1, v2_version=4, v23_sep='/', padding=None, preserve_mtime=False) Save changes to a file. @@ -241,6 +242,8 @@ def save(self, filething=None, v1=1, v2_version=4, v23_sep='/', if v2_version == 3. Defaults to '/' but if it's None will be the ID3v2v2.4 null separator. padding (:obj:`mutagen.PaddingFunction`) + preserve_mtime: + Keep the original file modified time as it was before saving. Raises: mutagen.MutagenError @@ -252,6 +255,10 @@ def save(self, filething=None, v1=1, v2_version=4, v23_sep='/', """ f = filething.fileobj + + filename = filething.filename + if filename is not None: + original_mtime = os.stat(filename).st_mtime_ns try: header = ID3Header(filething.fileobj) @@ -270,8 +277,13 @@ def save(self, filething=None, v1=1, v2_version=4, v23_sep='/', delete_bytes(f, old_size - new_size, new_size) f.seek(0) f.write(data) - + self.__save_v1(f, v1) + + if preserve_mtime is True and filename is not None: + new_atime = os.stat(filename).st_atime_ns + print("\nRetaining original mtime. file={}, atime={}, o_mtime={}".format(filename, new_atime, original_mtime)) + os.utime(filename, ns=(new_atime, original_mtime)) def __save_v1(self, f, v1): tag, offset = find_id3v1(f) diff --git a/tests/test_id3.py b/tests/test_id3.py index 3b048baa..57308233 100644 --- a/tests/test_id3.py +++ b/tests/test_id3.py @@ -2,6 +2,8 @@ import os from io import BytesIO +import time + from mutagen import id3 from mutagen import MutagenError from mutagen.apev2 import APEv2 @@ -893,6 +895,51 @@ def test_save(self): self.assertEqual(frame.encoding, 1) self.assertEqual(frame.text, strings) + def test_retain_mtime(self): + + def run_test(label, flag): + + print("\n" + label) + + file = get_temp_copy( os.path.join(DATA_DIR, 'silence-44-s.mp3')) + audio = ID3(file) + + # import datetime + # initial_mtime = datetime.datetime(2021, 1, 1, 12, 0, 0).timestamp() + # os.utime(file, times=(initial_mtime, initial_mtime)) + + mtime_before = os.stat(file).st_mtime + if flag is False: + time.sleep(0.1) + audio.save(v2_version=3, preserve_mtime=flag) + mtime_after = os.stat(file).st_mtime + print("\nfile {}, before={}, after={}".format(file, mtime_before, mtime_after)) + if flag: + tolerance = 0.1 + self.assertTrue(abs(mtime_after - mtime_before) < tolerance, "mtime difference greater than tolerance") + #self.assertEqual(mtime_before, mtime_after) + else: + self.assertNotEqual(mtime_before, mtime_after) + + + run_test("file mtime will be preserved", flag=True) + run_test("file mtime will not be preserved", flag=False) + + + # audio = ID3(self.filename) + # mtime_before = os.path.getmtime(test_file_name) + # audio.save(v2_version=3, preserve_mtime=True) + # mtime_after = os.path.getmtime(self.filename) + # self.assertEqual(mtime_before, mtime_after) + + # audio2 = ID3(self.filename) + # #time.sleep(1) + # mtime_before_2 = os.path.getmtime(test_file_name) + # audio.save(v2_version=3) + # mtime_after_2 = os.path.getmtime(self.filename) + # self.assertNotEqual(mtime_before_2, mtime_after_2) + + def test_save_off_spec_frames(self): # These are not defined in v2.3 and shouldn't be written. # Still make sure reading them again works and the encoding From 12411db2ad8049dc14c9f4c1f8d07b31d2ebe520 Mon Sep 17 00:00:00 2001 From: kjc1 Date: Sun, 10 Mar 2024 22:04:42 +0000 Subject: [PATCH 2/6] Retain file mtime using flag in ID3 save --- mutagen/_util.py | 31 ++++++++++++++-------- mutagen/id3/_file.py | 7 ++--- tests/__init__.py | 9 +++++++ tests/data/silence-44-s-aged-filetime.mp3 | Bin 0 -> 16384 bytes tests/test_id3.py | 28 +++---------------- 5 files changed, 34 insertions(+), 41 deletions(-) create mode 100644 tests/data/silence-44-s-aged-filetime.mp3 diff --git a/mutagen/_util.py b/mutagen/_util.py index b99c7c78..8420479d 100644 --- a/mutagen/_util.py +++ b/mutagen/_util.py @@ -16,6 +16,7 @@ import codecs import errno import decimal +import os from io import BytesIO from typing import Tuple, List @@ -271,17 +272,25 @@ def _openfile(instance, filething, filename, fileobj, writable, create): else: raise MutagenError(e) - with fileobj as fileobj: - yield FileThing(fileobj, filename, filename) - - if inmemory_fileobj: - assert writable - data = fileobj.getvalue() - try: - with open(filename, "wb") as fileobj: - fileobj.write(data) - except IOError as e: - raise MutagenError(e) + try: + with fileobj as fileobj: + yield FileThing(fileobj, filename, filename) + + if inmemory_fileobj: + assert writable + data = fileobj.getvalue() + try: + with open(filename, "wb") as fileobj: + fileobj.write(data) + except IOError as e: + raise MutagenError(e) + finally: + if hasattr(fileobj, "__restore_mtime__"): + new_atime = os.stat(filename).st_atime_ns + original_mtime = fileobj.__restore_mtime__ + print("\nRetaining original mtime. file={}, atime={}, o_mtime={}".format(filename, new_atime, original_mtime)) + os.utime(filename, ns=(new_atime, original_mtime)) + else: raise TypeError("Missing filename or fileobj argument") diff --git a/mutagen/id3/_file.py b/mutagen/id3/_file.py index 123957ee..8176b627 100644 --- a/mutagen/id3/_file.py +++ b/mutagen/id3/_file.py @@ -257,8 +257,9 @@ def save(self, filething=None, v1=1, v2_version=4, v23_sep='/', f = filething.fileobj filename = filething.filename - if filename is not None: + if filename is not None and preserve_mtime: original_mtime = os.stat(filename).st_mtime_ns + setattr(f, "__restore_mtime__", original_mtime) try: header = ID3Header(filething.fileobj) @@ -280,10 +281,6 @@ def save(self, filething=None, v1=1, v2_version=4, v23_sep='/', self.__save_v1(f, v1) - if preserve_mtime is True and filename is not None: - new_atime = os.stat(filename).st_atime_ns - print("\nRetaining original mtime. file={}, atime={}, o_mtime={}".format(filename, new_atime, original_mtime)) - os.utime(filename, ns=(new_atime, original_mtime)) def __save_v1(self, f, v1): tag, offset = find_id3v1(f) diff --git a/tests/__init__.py b/tests/__init__.py index 40919647..c6f0e8de 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -34,6 +34,15 @@ def get_temp_copy(path): shutil.copy(path, filename) return filename +def get_temp_copy_keep_metadata(path): + """Returns a copy of the file with the same extension""" + + ext = os.path.splitext(path)[-1] + fd, filename = mkstemp(suffix=ext) + os.close(fd) + shutil.copy2(path, filename) + return filename + def get_temp_empty(ext=""): """Returns an empty file with the extension""" diff --git a/tests/data/silence-44-s-aged-filetime.mp3 b/tests/data/silence-44-s-aged-filetime.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..8472632917f65c168bc2020a9df74ad34e0c2595 GIT binary patch literal 16384 zcmeHubySpZxAwyjqA<+RNDhcdNlBNqgdiL~^>YT`TT1Y0;_Bgi#nIXHtS0;uauot$0NQwi|xzrMV&x*qW##3V$;#lT;{ zFYqsa0iONu$NzfZe?9QO9{B(0frCA)6NK3efRrE=2%^Cs!jtjPk-Q-%LnYWj;&_}2 zZ31+9;GqFmHUIlxp%K1I`?>pA-^dM(b65uIt9SPFqG!q^-c5gH(QgW3&uCmdwaOIN zlUbi@6eu!wgr?umIQ;pW$Or4Z2YYl*5A32Su1JJ zSI*6TX`vq{KTRFxe!)H}U|uIglaWMyvYu3=i`8Pbv_i6Kq+^qw-MHGS=v_Szn^kVc z7d`e&D(+#=_!bxtBWBYLdV=o;jw?j$l#)}RfCWVe2Yv{Z%)mO@Ek2#B;`5 z`s%%3Nmm|9di1T^do@73N8?)C$&x8{XJh+T8UOf8WG_so9d-qDO$D2ZO9SI7=UBJf z_h3M5$(!yZYH)-w5Q`WY0Q3+sq=b0xkYOfgb%v=E%|X$Ha}!G;=oFKz*6cH$gGwn{ zf=WyR5fYcPTblGL26Gb0ng>=b(L#<~t}-PKlbqgv?qP!&pI_N{3IoC=Z#n_T0Xt|r z1khJ-E^o z?Vx4FkPJU?baRhv7I3*X1#gkHcdji0zuG zyy>!ez?&ektzILK4!xW$-Ac)_MA_FT;wCv4;+LYYYse*~J=>dAT;rG> za7;6Hs#mM_-KP?V0$mu8C}uMRiO`WkN&*wQ5y#0P5wb4Rq#2WOjanyL?GXmZ;V0$-G~1K9ebMuo7g&C~51650 zK+>2^6C^^^c*M{kLOB@(P_&oeJ33eQLxuvSX5s<->9OLNAY&oZ38RTZ|JpAP;Ug29XmidJ4ze~3CV48 zN;pDNcmm}((YZ4%W6vwzow{w8A}?vZ+TYjAQ{)hF+md?xE^~V|&rPwTC!ItCHpa@> zJue4*qM>EpiX6_!<51&&&64`ffAmU5>km*r+f} z+FC6ce7x$AD0B5{X<5JTUZN*^R28#&_G|rE{hhz`ndK{$o>;-bPJi@jt!g6=zuM3o ziD`71$Z6TkPE8n)Uh<|Lh>!{Z)QtdICZ&LzHhI`_vD%(D1}a+;y7I}hIVBDo7?vgZ zT8>sFifcwOen@`!fT?RE(W1XC&5Fu48M6<|(BZ?0}wh_lY9Qs=5)+cQ~o*~G5ZLT0ycG`A! zFL{W+^hdg{=j>Cyc-ux&?R1Ovuf|(iHDoO*dW))mcqW>kH5~oK!877k{QH=^AR&i5 zlQ#pAWJMEmC=w9`30UP4pqRn>ex@<|SH34LV)`3}WF?0)dv{I68!dc3^tPuN`Q>-U z(|(T5DSP%bYray?xk)H27d=d8^J;F@WJ%C5+Zfy3PKZzdsCNjpX;fHn5D!E$NJ<7% z?j!;zv{E%SU=TX@Y61Vt%Tv0kT>NA8d8m1DvU{Y$yDqZv*ZmPWnyw=jvz)J5ez(e9 zUuLV^;$5rMGigzzyF1?gAv0#?yBZv!Pz>PqQ#eA@I7H7_ z{+0j<9zh|8$=cqW$aJ`Fo(Rs|BmeZ)-=Ytt%nSiY2m%R6E z<)fWO9Xi_|q6Q>!L3w5v_q3=(BKlKQR9wd6`vX*^AKnj3DZI5*Fph|49bU#Jz<|mS zQRoI0ybw{JLZsj+Af7`%%4|&5Y(mAgE{8;y8wHr}mtajhea_1zkr(0aak`lLuwdq3gUezs&6wC>Kvl`0XM6$yJBRHkRUiOp6sRe9 z6oR$8jiI)x-_NN@Q^Thm9C z9DPUKx2TPt>g()%6rsWPS8?d*J0S3yCo;hc}sZ8)rj*&;03cU6aiFzCd1L{uR zv_>j88iz!v9qAMl5Gr)&tKIg_6*PSKvU(~G<7&_|cx>FS#g^lKc0re(t`T;<8fC(UnVA)(&Z0cYHz>8} zPm&n3tXiw;VW)-nGJAZdpC9+jf1M^BnlXDOw6I&8R@D6(Bi}2!A2zO}?+40u1kSMB zbn%c@{wNItn!s#c27tOju66_QImSl<9ESoHK(`Isnc#PS_0{(i#qHld!;JpzoB}ji z0*hxVAKW`k8T{Sg2m|WjE8S&}kd!Da8egLx-BLv-&=Zr^-*HN?P^P|;RxYNP<7y-f zXb!XK`wt+5+cij!1yc?rt4nol2Y-dxi@fH1Vc!BK!+R=7v^@!(+dtIo4aLHUzP-cM}TOwjdjFYdyCmVnA3^$zap0Roama-+I~A; z7h>GqJ?^{QeZnM`e6x5RBiwJQyaEIIgxQP+$*KZDbcHNfAg8Loh$@Ffs5;AXFsXKK zf6hAnl4mgQ37YjDMwXv(D$mYOXgg?CIFRQw$D4Q4RR=GH7tbaf^~rnU9P!4Zx<=q7 z?&W1F^d~n|S`7^7GiI|0#Agvg(L*c{nIojej{rIYfjX2x5^`czHa5bI0i~rPJaptj zCXFTEI-eS|uX$35PQ}0n?ApyD&V$Rpr)Ph#)t>%s)0}=C6Zki|KIq45FZq)rR$Pk4 zjdqJLppXBzImH5%BReLGf&87`3(E}vpgoz-_)?%%xdgM%xJ@Ez|f7ae7wprvgRxex}W|F5dH}ZiU9`z zL6GV`+%wuiry-mQHa4d*M%+~*H*Ud|hNd+*l~j}kQ&0O@uEpn<4MWxEHd^-dBr`Q! z(b<8N+z8ol2Q4gv%7KYRuSBnJ>z{$@psJ1=SI2{&!GKnhHxm&#r7c9d;08n#)PX1f zW(0ZAU(J&zb)7!@a6{m3UG2fee)jE-LN+J<(YZ?;jvq|eb@W`vW;vtp@U3L9E-9&Q z%unCT=`XT(WPkX zPPUTyN>2INEXNB|yy^r%n9b`%ZUaCdSy6@pC{i*FyawEoKqSUCeg!!`$7|i+Ock>a z@5>c@%GaxUJa}$V zhg3+wfPmxT5K*9Aq{PBZpag&b6HbGA2LL{#Tps9Z9JuzmM$scQ5p&ulKzaMuLDMPP zPzh4cThk81><-ch+VfF z3OCR?d_-Npe$bXBzsGYRxd9kI9?20H7x<5Um|x0uO{D z1Gx$)2c%pD*roMRW#rzT!grGY!tW()^D|?z%7(9}tp(qIMsdVkQgNE*jQ_^_cc+5s z^sk*MoqjO4v+;Oz!(B+etc+sKBm6S^$Z>r>BISaJo+HSqEXtY`MTUnc&%)eJ$SteS zrVk`vi?4WdpuCY(u^|qdPpgi zXr=Y-BywzXRPM3Mof5UHVA(A8uhF^dnzO|hBe^&fb9>z0XvUYTX5b>%#e=15xy4h4 zX)ph5)DZ!tLEZQ#SQLm5M14$F6^!7p=iv1cNdhD+eyz|;o<(TV^|yz!h~<@j^rJt2 z)ym7>F$xTuJ9Yf3FqbeEc^UkEpxyS{2D`=R&dL3^eqVZ+NB&M>FORn7dSGK(6B9iItG6i|%U=7vK2qYZ)8tHY)^%1yFp}PICSv`%eAdNom2K z;q^LgcRXURYAXueF52w!@YzW}+YtJxDTB;gE}PZ&f!@>%OM4Lv2*_yxi3;?T1w}_9 zvq^T0m!uC+0gyrycn(nE)01L_go^813ukFI4YlHjV$#sx9r4L@35+S2vn`Gnwwtu7 z8_mm2(LB?aXl?ML%YAKHBKE_Fiautw4DBj*TTF5Ye)`PV(~XPAo;lxY-mAnYCSFf_ zZR>O9;!an+QIp7_by59+XRK2jT+ug|ScB6F?)=7Hr#qy;-=wz8Tkxp~G(N70OLYb@XX-pR(u z<>5Tcuj(&fb~q@$Rlr1(JkDRzTJEQm%eAPa2e$*xsiu52LNjv{sEyh5L^={$2m$1R z7}5rSXsL4_aD~a&>17860ef7lk8*0YR|iK*U2J20EnbFj{jNz-Kr;fhMwWs6?tYfDXdzxd}ow z5y{Ll+kb*Q6q>Kv1)dPG*x?CxuDqPUDH|g`O^8tPW-`oeEJWK4>JX1CNgvpp0zevg zm~v2^NOYMOU#5xE%i6M>Ei4ZvDxzN~(S`?qqHenAb8xK7VNwwhk=^GWDD0$R>C4e~ z6awlg%5AC^&w5PUGPM1R;w}1MK%kY&23iWlhhDW00(Di5_a8t&O%q6-Jjv#!v>)!Y zwLH|%b)2rDdXi(LMLnIrrO+xpKuxB@UEN{9M$=8;0$cDA*G^SF(jp~#!Nu&`y$3>J zuAB`AJAUU<30*yCv0!d@!z*V(M}pD-^$s8*=FqqDKgRo6iuKHE`O>onCl~%u(umHy z4V%N?U)WI)-neu6gqx*4>->*ylKu#_Z)%G@<(sHpiay(-v~#-V6|SqF^`8(Hn!rv3 zpd1h%;tm}yL!_V#0Vd!9pcKyPX`d@3^!EO8YSB2O5Hoa_h@fOmhmP;hO4JbD@)J45 zjeyLAor2zNuNRlHZgA~$1~fVdm17m7UuM**Py9S7pm2^rIRFUBA*>4M89}l#AS!nn zs2tcX;kB96tRub(=#6{km}%asLYEg0damS?7U7DmEgUP%Dg_wB6O45o?)Mp`@X=f3U$LZYiSt2 zWo6tutL8Ql=P7(vnX2I6O!KwJMIovBJCmm-X=`N&IfV3#Dqz$vMC8;PP=U;jQXkX@ zYyt_7*ZcaMTS0xf(i?+*CGVzZL)iDeU7w3RjLFyQx%9Du|L{=`6CJ<#hu`ko1=fyx zSgIC%y2Te)RBdQu_AdQ%k7ln3XcP!7RuaK+gn9=Ll8eY7se<&3o_{xTwQ!z4L$~!k zm$-~%9vgFhQcy*mT=<~ru7NEn(42k1C{?n%Vzus{O z`&BLbRp)q3JHs`%AO5cQ+~4Q+aUN*7c06v^t=M|#OI6@fR_l|*;&TBTUS&p%P%I+b zSRg*Y4$2US0&tuz5FgOWS^J-Susy$bt>vxP71f@+Use||m0v@G8uG?w*K7*+1WbOM zv$MR)}z;63qg;5eYyJEc=?tqA%{S>O=x`}%pyVsemFuZ z5bQZ%S7Bhy>tyCffyYj(+VuSz)vl)h{DIE|WkV9%3ggOLzMp#WrB_?CVx`_J3X-U=?GF_N^+!$-*JTH7tE4rd=F<`BHw?gkaSn-C$@U&v%k?6#}e z!eHoKVDlRusORdU+mx9-b^v}*epo81~l7ws~R?OsM*kpH+~kKk)fATh31W!jvFN|;PbGGubw8e zUN!qd1mukX!T>iO*5-oF?)BKtcEljtDGIo^Z`eiqBp%|T_b zJ-VHwK6-AX#Mm)0B)MtQRSEhOM5?F)#r`wa_rlcv;Bnvmqn|^c3%Bm z51K!7abD_;f4u!}FBNM^T$v+hR8Ez2$+V~E z3Z{begVt!C(ObqD3Iz2ZeeFv=gX)Em zO1zUGo{&Sy@Td=ajuvnmf_DmpWQDZj3&E0P>9fl@F{ztn0~a12GR@?q5yZHY#}|2- z1VDBCa+>St&zwwN2PNe1C9RYMO@pn$G?sblhZ$!?X`X|@w)GUa~2U$Bw~jK zhyt*K(ndt#EJC@32XznL&AokbIx4e6({LZX=PO!;PDpWiV&ZrL*HH3C{!5^MiS*QO z-vF*!FLZ5R94$%AXxcYJm3gf)t#+H?4s8`FP9mTvpgsT)eBcEe0&6T39s&^}>=dN$ z3W|TQ5HQ3w-?5X=E{$_^L+41gw$ujGByp6Y0@NbnBI}!uY!Cf?%r5*`+G)M^9OfXJ z&2a3SDXnJ=9U1#p**See4q-NlIfV3#bZ~iqTP(P+(#9^{8aKZ1`s z8F(x@dvucF@=wvOw@hIRWAF7X-qroN%&x$V7Tl!vSc!J}IBY0uxeNn>_kXZ#gUOiG z1p-YlSjG@Gw3-m;ND@dE$}zHY7OM5?*-%9lrEtj{59hK{G_~y|>o>V?64Wv=KDXva z4Ay__9X#X?3hUP}5%nwcejimWTF>34+}mK17S^!uNsQ1{VzL$@Q~+x%Qn0I4hN%#{ zxbV7vXY*}--_7bn?-^+>Z_eGA9-YVzi1t_}oBnen*E}KCDVcnV@y`CnX{XTbI*v!? z3~Yi^7ndOE($*>r-_NjGf(6Q_6XgW|5a?u~`z zn8)jNT^-28iqxS(_`nMRgiMkIDu4(n>ldg#tvR#2Rv&NMHCSLf`HS-fdySLh&!%$v z5b+2-ZKbQ2vy4WcWU%h)UbnFJYl%p}bzXlf;M93j{T?X&}Ah2g7 zhpuI0-OY#hC4%2mq}1I zE49Q8QyjUztG=qvP^7?J$c4GWA#GtM@sznG)%Z}`hH=EfPTrJScfeK!+86h^16J#-&Ra-tVz(rlo{z7N;rmtEP55P`*l9}GB#f4WaPRHy$T4;(NN0gNv4|=QoxNf%RSKaTeqeJbo*SdP& zmnGgkueZ_GC&75mP+IkHl>n3Q6qVGlUQQm%y*y$L5f_>m54uo%vN~-8Fs4JbqhW|2qAsQuy;i3D#3q3mLhcMO`Mb zr-^_-j|G7d91Bzq05cUiHJJU!bi7UHesfTVO(b?NP8^i!tG}9f(2-vD;n* zhXM`|*j*6HgE{~N?8LW6Y|wd7-&1d{@JKh~QplY0YQ$XWF`U!Sl4Iyh6;#^a~#HMTLt*xt}@be#QkP?lnwfJ0@HnRLsl@$K#AYve4# z+%<|Dq_do{YxYgW%yC@4QFnXAoru)_r+|YdnCut>$ueL9nF>J^K&~ouZDwYNe7u%Z z6!Z7|TI=*0r|>{myhS^CJlRc_tkzMtENRDF1 zUzGgwIn^MgV1+UA%JOt_xL`x01L+&+{A}^j+e1)Ri=CdSdwo97#BQ6bAc*f&zhw^5i za=7YK1}uK$gf}ry2ej1e*fe|&co+Re8YQFWP@OQE`|7X|0T5<0l~^9|gu@3lYJ>@A z0OP>G>fAPV^qk+9*tYTQ4-cR1JRz?_cW4`bv@iK`wZ^#d%p=hZ$2{``Rgd@Yo>a}) zUrI_w{|(}AKP2^KZ2!@0B{sgfMreAR7@-tIwt@J-vaR|Rs44KAG*ZBA#^;{dUhbK; zJj|F=T@ZY9J*Bro^rF`RXStvb1po%7Lwq zJ8>QYr}_X8oI^QDeSa@|^u!ul4kC&Vv&7VS(_yB$;no>(``kE1L z7Aa+6*=uCn@~mdBB6&a;Mj9%}#FMtdteqAq@`ckV90s(4*$hHd8v}Y%P{2{KR1j$b z4xWr5P{7X@32M}4%qZU{n|zpQhJNjJyIZ|0f9gWptD2enYnpKnT*K}gDLOt^I67}R z*l3oMKiztK8B-0}#`U??rF`3o@3)vtAV!F|^`Tuv4DkZb0buZzQxGxhQ&zTz+E?#+ zsvV=KeZlfxkVbfd)`&^=9nBTjN6~yD={lv3FPfB9g6Y!bSgqyM;`ul+)35XOs;?Co z_}pKy+0Ar8Hnho`;eZL@~`_j5U1AR+9s6jc=4Y#Ht@Y8WWoEWTkTuT86;TBB+sp!J&w z2#=^7y#IrHId7!(L5_!(P{U$A@kWEP5ww@2y|vkg9ro2t+sxZ8p5AUd;#teMTOEA= z-fz9{`|C|rm52LR&WYBY%#D~mt-Z4-KmS)!ZRXd2_y!yyz=RMahtTkt0wUy14v~VZ za|RF&z#{ScnEF|Lyl*>92&T|DC{(0)t)oQ#dj6EyoGWk}engg!5Gf3(6 zVB=CPwzqEhj$xJGzBLTJ^5{d}hS0s(WTGhG;qm}(AA^nrJ`_kGg~%f9D2+)Af%@pY zn%|jBJ{4tdChgYO^8C)&%10XXL+@Gvn;$<<>m15-WGtr@6#p67IjkO`uld%eDO#IX zz9iN>J)eSsDT}7~gszou7!eS>awhEm0H9+KXo3ml;S}BugbiZTk^I@QkN@~asFNb> zxMg0A}OK2vEs2kD*LwrhOf z9{c3_to*)wFxa9b!EH1Xb!5I`x2>b@*L7+&vLSu2osS>*VRUqh439*G9Mf`0@Cyae>HdpO@|t zK2~+sU*?~;$}6pN0nZUOv?(B2VJhI^mV{0TyHMBaJz(U*~F{&FLlu_srp6coj>_X zyvjs~l*q{Nuv_*D^MJf-Sh(afr6xi>wFcKpKdk#d1 z=s9roNw}A%29g!jA=PpnLF%NAa2{OfqqF1hS2_7t6n*v0ys7UG9)BvAJDb9cdZ0MI zL?28hK%w?R&}>n?-1szkf1KDrCY7D$h)!!(d^QXSG{KpO$X&i0x~siY>h#+6lEcMCUn&Pv(5MbB^_s zayaQwdgb){AsXuAj;MEn-4sMX$pk~-I0ae8$Rcv8Mwo{n+>liD>+Fxg4lmYyFM8jY zr4HP|X!8A(Z%O_IpBX>$6v=kCPUSV{nId5#$h{HX%Z>Bnz$_ zKTs3N@f5}6({GH3Ddg}ah(S8wlLzY}`0^VspJ*L~N{we|S7<|*!XRkksIs>g73 z#vD~(2)%Eb6ltF7hZ{!@r;|6~!bWnq$!7xQst4t;hx zk?x*;d4qnVj)1%A%VIZ+>#D6a7Dnq*%pt`J#vLoNHuVHi00(Q~2q4&V@WI+Yhtoji zfEJ1C>nmP(Q)b_*&hG=AdO3-y*YJDU{``rKec*(c^o!iPVhvv5e?A?w-FdXl_HANJ zS1y04C8mljCw?Ki$UE)}C9lc}0_8B9uEfIH4eY2xC?`Zs!9o+Z!?#2$-jT)$g+(iL6xs)DgRO7(6DjQR#F-`_``)XlrI!t#Otp}CGw zZu5-idzMs~a-hfZLk!V?i-u$>U>@QIgO?f@B*9K0fyAola5wwVO@>2h9MBorL7jKh!2(Z3TNqu$D`iiYrdUP<+lopD9SCJ>G9^g#Pw14lCmAynHvS_UXQ< zvi8)7em`3JP06Ldf4+5}n+|&Ag~q5Xv(mcEC{;7JMkVCgKZXH;)(77DyaP95xhX4ILAErY`Z744QA%dhh=RU2FdnlGTr;Dt(scpmX2pnN7K^ z%q;md$K{Y>ud44fXwoyShdn=J9f>X+6=Y4@gaH92!~i?+N&(DWpge$sHLz!d?=Ios zPT~BMlvl<*s`oO3=x*MNZQee9R#ji~l!)A^s(<l|A*}A)qx#Ax&QRF|Ly+>luHKhQ&NTO!0uuZ*{{is<$4-zM3-k*Du~CIMyJ-^^|HGI5_kSZ&4uV{P&VUaz zj_^QIN_c=I@!;{_|KmS Date: Mon, 11 Mar 2024 12:09:59 +0000 Subject: [PATCH 3/6] Moved setter to _util.py --- mutagen/_util.py | 5 +++++ mutagen/id3/_file.py | 7 +++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/mutagen/_util.py b/mutagen/_util.py index 8420479d..57acd947 100644 --- a/mutagen/_util.py +++ b/mutagen/_util.py @@ -294,6 +294,11 @@ def _openfile(instance, filething, filename, fileobj, writable, create): else: raise TypeError("Missing filename or fileobj argument") +def set_restore_mtime(filename, fileobj): + if filename is not None and fileobj is not None: + original_mtime = os.stat(filename).st_mtime_ns + setattr(fileobj, "__restore_mtime__", original_mtime) + class MutagenError(Exception): """Base class for all custom exceptions in mutagen diff --git a/mutagen/id3/_file.py b/mutagen/id3/_file.py index 8176b627..ea8fd74c 100644 --- a/mutagen/id3/_file.py +++ b/mutagen/id3/_file.py @@ -11,7 +11,7 @@ import mutagen from mutagen._util import insert_bytes, delete_bytes, enum, \ - loadfile, convert_error, read_full + loadfile, convert_error, read_full, set_restore_mtime from mutagen._tags import PaddingInfo from ._util import error, ID3NoHeaderError, ID3UnsupportedVersionError, \ @@ -257,9 +257,8 @@ def save(self, filething=None, v1=1, v2_version=4, v23_sep='/', f = filething.fileobj filename = filething.filename - if filename is not None and preserve_mtime: - original_mtime = os.stat(filename).st_mtime_ns - setattr(f, "__restore_mtime__", original_mtime) + if preserve_mtime: + set_restore_mtime(filename, f) try: header = ID3Header(filething.fileobj) From f8f2ae262fc34867eaa11b3b3d11cecaded0150a Mon Sep 17 00:00:00 2001 From: kjc1 Date: Mon, 11 Mar 2024 12:53:53 +0000 Subject: [PATCH 4/6] Current atime. Moved logic to _util.py --- mutagen/_util.py | 9 +++++---- mutagen/id3/_file.py | 3 +-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/mutagen/_util.py b/mutagen/_util.py index 57acd947..4c395a29 100644 --- a/mutagen/_util.py +++ b/mutagen/_util.py @@ -17,6 +17,7 @@ import errno import decimal import os +import time from io import BytesIO from typing import Tuple, List @@ -286,7 +287,7 @@ def _openfile(instance, filething, filename, fileobj, writable, create): raise MutagenError(e) finally: if hasattr(fileobj, "__restore_mtime__"): - new_atime = os.stat(filename).st_atime_ns + new_atime = time.time_ns() original_mtime = fileobj.__restore_mtime__ print("\nRetaining original mtime. file={}, atime={}, o_mtime={}".format(filename, new_atime, original_mtime)) os.utime(filename, ns=(new_atime, original_mtime)) @@ -294,9 +295,9 @@ def _openfile(instance, filething, filename, fileobj, writable, create): else: raise TypeError("Missing filename or fileobj argument") -def set_restore_mtime(filename, fileobj): - if filename is not None and fileobj is not None: - original_mtime = os.stat(filename).st_mtime_ns +def set_restore_mtime(fileobj): + if fileobj is not None: + original_mtime = os.stat(fileobj.name).st_mtime_ns setattr(fileobj, "__restore_mtime__", original_mtime) diff --git a/mutagen/id3/_file.py b/mutagen/id3/_file.py index ea8fd74c..5a9a68e8 100644 --- a/mutagen/id3/_file.py +++ b/mutagen/id3/_file.py @@ -256,9 +256,8 @@ def save(self, filething=None, v1=1, v2_version=4, v23_sep='/', f = filething.fileobj - filename = filething.filename if preserve_mtime: - set_restore_mtime(filename, f) + set_restore_mtime(f) try: header = ID3Header(filething.fileobj) From 0e5dd4c3a730b37a9fddaa749c372ff79b90cbc4 Mon Sep 17 00:00:00 2001 From: kjc1 Date: Tue, 2 Apr 2024 23:16:14 +0100 Subject: [PATCH 5/6] Other formats --- mutagen/_iff.py | 6 +++++- mutagen/apev2.py | 7 +++++-- mutagen/asf/__init__.py | 7 +++++-- mutagen/dsf.py | 8 ++++++-- mutagen/easyid3.py | 9 +++++---- mutagen/flac.py | 8 ++++++-- mutagen/mp4/__init__.py | 10 +++++++--- mutagen/ogg.py | 10 +++++++--- mutagen/wave.py | 7 ++++++- 9 files changed, 52 insertions(+), 20 deletions(-) diff --git a/mutagen/_iff.py b/mutagen/_iff.py index cdc6e3d8..d0ebcb64 100644 --- a/mutagen/_iff.py +++ b/mutagen/_iff.py @@ -20,6 +20,7 @@ delete_bytes, insert_bytes, loadfile, + set_restore_mtime, reraise, resize_bytes, ) @@ -364,11 +365,14 @@ def _pre_load_header(self, fileobj): @convert_error(IOError, error) @loadfile(writable=True) - def save(self, filething=None, v2_version=4, v23_sep='/', padding=None): + def save(self, filething=None, v2_version=4, v23_sep='/', padding=None, preserve_mtime=False): """Save ID3v2 data to the IFF file""" fileobj = filething.fileobj + if preserve_mtime: + set_restore_mtime(fileobj) + iff_file = self._load_file(fileobj) if 'ID3' not in iff_file: diff --git a/mutagen/apev2.py b/mutagen/apev2.py index 4c57d94d..c748e275 100644 --- a/mutagen/apev2.py +++ b/mutagen/apev2.py @@ -36,7 +36,7 @@ from mutagen import Metadata, FileType, StreamInfo from mutagen._util import DictMixin, cdata, delete_bytes, total_ordering, \ - MutagenError, loadfile, convert_error, seek_end, get_size, reraise + MutagenError, loadfile, convert_error, seek_end, get_size, reraise, set_restore_mtime def is_valid_apev2_key(key): @@ -396,7 +396,7 @@ def __setitem__(self, key, value): @convert_error(IOError, error) @loadfile(writable=True, create=True) - def save(self, filething=None): + def save(self, filething=None, preserve_mtime=False): """Save changes to a file. If no filename is given, the one most recently loaded is used. @@ -407,6 +407,9 @@ def save(self, filething=None): fileobj = filething.fileobj + if preserve_mtime: + set_restore_mtime(fileobj) + data = _APEv2Data(fileobj) if data.is_at_start: diff --git a/mutagen/asf/__init__.py b/mutagen/asf/__init__.py index a756a691..43fdee28 100644 --- a/mutagen/asf/__init__.py +++ b/mutagen/asf/__init__.py @@ -11,7 +11,7 @@ __all__ = ["ASF", "Open"] from mutagen import FileType, Tags, StreamInfo -from mutagen._util import resize_bytes, DictMixin, loadfile, convert_error +from mutagen._util import resize_bytes, DictMixin, loadfile, convert_error, set_restore_mtime from ._util import error, ASFError, ASFHeaderError from ._objects import HeaderObject, MetadataLibraryObject, MetadataObject, \ @@ -245,7 +245,7 @@ def load(self, filething): @convert_error(IOError, error) @loadfile(writable=True) - def save(self, filething=None, padding=None): + def save(self, filething=None, padding=None, preserve_mtime=False): """save(filething=None, padding=None) Save tag changes back to the loaded file. @@ -300,6 +300,9 @@ def save(self, filething=None, padding=None): header_ext.objects.append(MetadataLibraryObject()) fileobj = filething.fileobj + if preserve_mtime: + set_restore_mtime(fileobj) + # Render to file old_size = header.parse_size(fileobj)[0] data = header.render_full(self, fileobj, old_size, padding) diff --git a/mutagen/dsf.py b/mutagen/dsf.py index 121a01ba..ced09039 100644 --- a/mutagen/dsf.py +++ b/mutagen/dsf.py @@ -14,7 +14,7 @@ from mutagen import FileType, StreamInfo from mutagen._util import cdata, MutagenError, loadfile, \ - convert_error, reraise, endswith + convert_error, reraise, endswith, set_restore_mtime from mutagen.id3 import ID3 from mutagen.id3._util import ID3NoHeaderError, error as ID3Error @@ -198,10 +198,14 @@ def _pre_load_header(self, fileobj): @convert_error(IOError, error) @loadfile(writable=True) - def save(self, filething=None, v2_version=4, v23_sep='/', padding=None): + def save(self, filething=None, v2_version=4, v23_sep='/', padding=None, preserve_mtime=False): """Save ID3v2 data to the DSF file""" fileobj = filething.fileobj + + if preserve_mtime: + set_restore_mtime(fileobj) + fileobj.seek(0) dsd_header = DSDChunk(fileobj) diff --git a/mutagen/easyid3.py b/mutagen/easyid3.py index 510b9222..7018ec17 100644 --- a/mutagen/easyid3.py +++ b/mutagen/easyid3.py @@ -174,8 +174,8 @@ def __init__(self, filename=None): @loadfile(writable=True, create=True) def save(self, filething=None, v1=1, v2_version=4, v23_sep='/', - padding=None): - """save(filething=None, v1=1, v2_version=4, v23_sep='/', padding=None) + padding=None, preserve_mtime=False): + """save(filething=None, v1=1, v2_version=4, v23_sep='/', padding=None, preserve_mtime=False) Save changes to a file. See :meth:`mutagen.id3.ID3.save` for more info. @@ -191,12 +191,13 @@ def save(self, filething=None, v1=1, v2_version=4, v23_sep='/', self.__id3.update_to_v23() self.__id3.save( filething, v1=v1, v2_version=v2_version, v23_sep=v23_sep, - padding=padding) + padding=padding, preserve_mtime=preserve_mtime) finally: self.__id3._restore(backup) else: self.__id3.save(filething, v1=v1, v2_version=v2_version, - v23_sep=v23_sep, padding=padding) + v23_sep=v23_sep, padding=padding, + preserve_mtime=preserve_mtime) delete = property(lambda s: s.__id3.delete, lambda s, v: setattr(s.__id3, 'delete', v)) diff --git a/mutagen/flac.py b/mutagen/flac.py index bde374b4..dc1d03e8 100644 --- a/mutagen/flac.py +++ b/mutagen/flac.py @@ -27,7 +27,7 @@ import mutagen from mutagen._util import resize_bytes, MutagenError, get_size, loadfile, \ - convert_error, bchr, endswith + convert_error, bchr, endswith, set_restore_mtime from mutagen._tags import PaddingInfo from mutagen.id3._util import BitPaddedInt from functools import reduce @@ -836,13 +836,14 @@ def pictures(self): @convert_error(IOError, error) @loadfile(writable=True) - def save(self, filething=None, deleteid3=False, padding=None): + def save(self, filething=None, deleteid3=False, padding=None, preserve_mtime=False): """Save metadata blocks to a file. Args: filething (filething) deleteid3 (bool): delete id3 tags while at it padding (:obj:`mutagen.PaddingFunction`) + preserve_mtime (bool): Keep existing modified time on save If no filename is given, the one most recently loaded is used. """ @@ -856,6 +857,9 @@ def save(self, filething=None, deleteid3=False, padding=None): raise ValueError("Invalid seektable object type!") self.metadata_blocks.append(self.seektable) + if preserve_mtime: + set_restore_mtime(filething.fileobj) + self._save(filething, self.metadata_blocks, deleteid3, padding) def _save(self, filething, metadata_blocks, deleteid3, padding): diff --git a/mutagen/mp4/__init__.py b/mutagen/mp4/__init__.py index 9e1757ec..fbaae5bd 100644 --- a/mutagen/mp4/__init__.py +++ b/mutagen/mp4/__init__.py @@ -32,7 +32,7 @@ from mutagen._constants import GENRES from mutagen._util import cdata, insert_bytes, DictProxy, MutagenError, \ hashable, enum, get_size, resize_bytes, loadfile, convert_error, bchr, \ - reraise + reraise, set_restore_mtime from ._atom import Atoms, Atom, AtomError from ._util import parse_full_atom from ._as_entry import AudioSampleEntry, ASEntryError @@ -389,7 +389,7 @@ def _render(self, key, value): @convert_error(IOError, error) @loadfile(writable=True) - def save(self, filething=None, padding=None): + def save(self, filething=None, padding=None, preserve_mtime=False): values = [] items = sorted(self.items(), key=lambda kv: _item_sort_key(*kv)) @@ -418,7 +418,11 @@ def save(self, filething=None, padding=None): except AtomError as err: reraise(error, err, sys.exc_info()[2]) - self.__save(filething.fileobj, atoms, data, padding) + + fileobj = filething.fileobj + if preserve_mtime: + set_restore_mtime(fileobj) + self.__save(fileobj, atoms, data, padding) def __save(self, fileobj, atoms, data, padding): try: diff --git a/mutagen/ogg.py b/mutagen/ogg.py index 263c939d..37296919 100644 --- a/mutagen/ogg.py +++ b/mutagen/ogg.py @@ -23,7 +23,7 @@ from mutagen import FileType from mutagen._util import cdata, resize_bytes, MutagenError, loadfile, \ - seek_end, bchr, reraise + seek_end, bchr, reraise, set_restore_mtime from mutagen._file import StreamInfo from mutagen._tags import Tags @@ -571,8 +571,8 @@ def add_tags(self): raise self._Error @loadfile(writable=True) - def save(self, filething=None, padding=None): - """save(filething=None, padding=None) + def save(self, filething=None, padding=None, preserve_mtime=False): + """save(filething=None, padding=None. preserve_mtime=False)) Save a tag to a file. @@ -581,11 +581,15 @@ def save(self, filething=None, padding=None): Args: filething (filething) padding (:obj:`mutagen.PaddingFunction`) + preserve_mtime (bool) Raises: mutagen.MutagenError """ try: + if preserve_mtime: + set_restore_mtime(filething.fileobj) + self.tags._inject(filething.fileobj, padding) except (IOError, error) as e: reraise(self._Error, e, sys.exc_info()[2]) diff --git a/mutagen/wave.py b/mutagen/wave.py index 6391a5d5..fc774966 100644 --- a/mutagen/wave.py +++ b/mutagen/wave.py @@ -22,6 +22,7 @@ endswith, loadfile, reraise, + set_restore_mtime ) __all__ = ["WAVE", "Open", "delete"] @@ -118,10 +119,14 @@ def _pre_load_header(self, fileobj): @convert_error(IOError, error) @loadfile(writable=True) - def save(self, filething, v1=1, v2_version=4, v23_sep='/', padding=None): + def save(self, filething, v1=1, v2_version=4, v23_sep='/', padding=None, preserve_mtime=False): """Save ID3v2 data to the Wave/RIFF file""" fileobj = filething.fileobj + + if preserve_mtime: + set_restore_mtime(fileobj) + wave_file = _WaveFile(fileobj) if u'id3' not in wave_file: From 03be74f4fd7fd60c4cf4db96712c9de8e39d79b3 Mon Sep 17 00:00:00 2001 From: kjc1 Date: Tue, 2 Apr 2024 23:30:23 +0100 Subject: [PATCH 6/6] flake8 changes --- mutagen/_iff.py | 4 ++-- mutagen/_util.py | 5 +++-- mutagen/apev2.py | 3 ++- mutagen/asf/__init__.py | 3 ++- mutagen/dsf.py | 3 ++- mutagen/easyid3.py | 3 ++- mutagen/id3/_file.py | 9 ++++----- mutagen/mp4/__init__.py | 1 - mutagen/wave.py | 5 +++-- tests/__init__.py | 1 + tests/test_id3.py | 19 ++++++++++++------- 11 files changed, 33 insertions(+), 23 deletions(-) diff --git a/mutagen/_iff.py b/mutagen/_iff.py index d0ebcb64..d39e0285 100644 --- a/mutagen/_iff.py +++ b/mutagen/_iff.py @@ -365,9 +365,9 @@ def _pre_load_header(self, fileobj): @convert_error(IOError, error) @loadfile(writable=True) - def save(self, filething=None, v2_version=4, v23_sep='/', padding=None, preserve_mtime=False): + def save(self, filething=None, v2_version=4, v23_sep='/', + padding=None, preserve_mtime=False): """Save ID3v2 data to the IFF file""" - fileobj = filething.fileobj if preserve_mtime: diff --git a/mutagen/_util.py b/mutagen/_util.py index 4c395a29..c486d2e3 100644 --- a/mutagen/_util.py +++ b/mutagen/_util.py @@ -289,12 +289,13 @@ def _openfile(instance, filething, filename, fileobj, writable, create): if hasattr(fileobj, "__restore_mtime__"): new_atime = time.time_ns() original_mtime = fileobj.__restore_mtime__ - print("\nRetaining original mtime. file={}, atime={}, o_mtime={}".format(filename, new_atime, original_mtime)) + print("\nRetaining original mtime. file={}, atime={}, o_mtime={}" + .format(filename, new_atime, original_mtime)) os.utime(filename, ns=(new_atime, original_mtime)) - else: raise TypeError("Missing filename or fileobj argument") + def set_restore_mtime(fileobj): if fileobj is not None: original_mtime = os.stat(fileobj.name).st_mtime_ns diff --git a/mutagen/apev2.py b/mutagen/apev2.py index c748e275..8e6864af 100644 --- a/mutagen/apev2.py +++ b/mutagen/apev2.py @@ -36,7 +36,8 @@ from mutagen import Metadata, FileType, StreamInfo from mutagen._util import DictMixin, cdata, delete_bytes, total_ordering, \ - MutagenError, loadfile, convert_error, seek_end, get_size, reraise, set_restore_mtime + MutagenError, loadfile, convert_error, seek_end, get_size, reraise, \ + set_restore_mtime def is_valid_apev2_key(key): diff --git a/mutagen/asf/__init__.py b/mutagen/asf/__init__.py index 43fdee28..efe9768c 100644 --- a/mutagen/asf/__init__.py +++ b/mutagen/asf/__init__.py @@ -11,7 +11,8 @@ __all__ = ["ASF", "Open"] from mutagen import FileType, Tags, StreamInfo -from mutagen._util import resize_bytes, DictMixin, loadfile, convert_error, set_restore_mtime +from mutagen._util import resize_bytes, DictMixin, loadfile, convert_error, \ + set_restore_mtime from ._util import error, ASFError, ASFHeaderError from ._objects import HeaderObject, MetadataLibraryObject, MetadataObject, \ diff --git a/mutagen/dsf.py b/mutagen/dsf.py index ced09039..bee2ba77 100644 --- a/mutagen/dsf.py +++ b/mutagen/dsf.py @@ -198,7 +198,8 @@ def _pre_load_header(self, fileobj): @convert_error(IOError, error) @loadfile(writable=True) - def save(self, filething=None, v2_version=4, v23_sep='/', padding=None, preserve_mtime=False): + def save(self, filething=None, v2_version=4, v23_sep='/', padding=None, + preserve_mtime=False): """Save ID3v2 data to the DSF file""" fileobj = filething.fileobj diff --git a/mutagen/easyid3.py b/mutagen/easyid3.py index 7018ec17..7affabeb 100644 --- a/mutagen/easyid3.py +++ b/mutagen/easyid3.py @@ -175,7 +175,8 @@ def __init__(self, filename=None): @loadfile(writable=True, create=True) def save(self, filething=None, v1=1, v2_version=4, v23_sep='/', padding=None, preserve_mtime=False): - """save(filething=None, v1=1, v2_version=4, v23_sep='/', padding=None, preserve_mtime=False) + """save(filething=None, v1=1, v2_version=4, v23_sep='/', padding=None, + preserve_mtime=False) Save changes to a file. See :meth:`mutagen.id3.ID3.save` for more info. diff --git a/mutagen/id3/_file.py b/mutagen/id3/_file.py index 5a9a68e8..2ec2af8a 100644 --- a/mutagen/id3/_file.py +++ b/mutagen/id3/_file.py @@ -18,7 +18,6 @@ BitPaddedInt from ._tags import ID3Tags, ID3Header, ID3SaveConfig from ._id3v1 import MakeID3v1, find_id3v1 -import os.path @enum @@ -223,7 +222,8 @@ def _prepare_data(self, fileobj, start, available, v2_version, v23_sep, @loadfile(writable=True, create=True) def save(self, filething=None, v1=1, v2_version=4, v23_sep='/', padding=None, preserve_mtime=False): - """save(filething=None, v1=1, v2_version=4, v23_sep='/', padding=None, preserve_mtime=False) + """save(filething=None, v1=1, v2_version=4, v23_sep='/', padding=None, + preserve_mtime=False) Save changes to a file. @@ -255,7 +255,7 @@ def save(self, filething=None, v1=1, v2_version=4, v23_sep='/', """ f = filething.fileobj - + if preserve_mtime: set_restore_mtime(f) @@ -276,9 +276,8 @@ def save(self, filething=None, v1=1, v2_version=4, v23_sep='/', delete_bytes(f, old_size - new_size, new_size) f.seek(0) f.write(data) - + self.__save_v1(f, v1) - def __save_v1(self, f, v1): tag, offset = find_id3v1(f) diff --git a/mutagen/mp4/__init__.py b/mutagen/mp4/__init__.py index fbaae5bd..5a6ba60c 100644 --- a/mutagen/mp4/__init__.py +++ b/mutagen/mp4/__init__.py @@ -418,7 +418,6 @@ def save(self, filething=None, padding=None, preserve_mtime=False): except AtomError as err: reraise(error, err, sys.exc_info()[2]) - fileobj = filething.fileobj if preserve_mtime: set_restore_mtime(fileobj) diff --git a/mutagen/wave.py b/mutagen/wave.py index fc774966..0605592a 100644 --- a/mutagen/wave.py +++ b/mutagen/wave.py @@ -119,14 +119,15 @@ def _pre_load_header(self, fileobj): @convert_error(IOError, error) @loadfile(writable=True) - def save(self, filething, v1=1, v2_version=4, v23_sep='/', padding=None, preserve_mtime=False): + def save(self, filething, v1=1, v2_version=4, v23_sep='/', padding=None, + preserve_mtime=False): """Save ID3v2 data to the Wave/RIFF file""" fileobj = filething.fileobj if preserve_mtime: set_restore_mtime(fileobj) - + wave_file = _WaveFile(fileobj) if u'id3' not in wave_file: diff --git a/tests/__init__.py b/tests/__init__.py index c6f0e8de..ec8ba14c 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -34,6 +34,7 @@ def get_temp_copy(path): shutil.copy(path, filename) return filename + def get_temp_copy_keep_metadata(path): """Returns a copy of the file with the same extension""" diff --git a/tests/test_id3.py b/tests/test_id3.py index edbe6899..34442147 100644 --- a/tests/test_id3.py +++ b/tests/test_id3.py @@ -17,7 +17,8 @@ save_frame, ID3SaveConfig from mutagen.id3._id3v1 import find_id3v1 -from tests import TestCase, DATA_DIR, get_temp_copy, get_temp_copy_keep_metadata, get_temp_empty +from tests import TestCase, DATA_DIR, get_temp_copy, \ + get_temp_copy_keep_metadata, get_temp_empty def test_id3_module_exports_all_frames(): @@ -898,26 +899,30 @@ def test_save(self): def test_retain_mtime(self): def run_test(label, flag): - - file = get_temp_copy_keep_metadata( os.path.join(DATA_DIR, 'silence-44-s-aged-filetime.mp3')) + + file = get_temp_copy_keep_metadata( + os.path.join(DATA_DIR, 'silence-44-s-aged-filetime.mp3') + ) audio = ID3(file) mtime_before = os.stat(file).st_mtime if flag is False: - time.sleep(0.1) + time.sleep(0.1) audio.save(v2_version=3, preserve_mtime=flag) mtime_after = os.stat(file).st_mtime - + if flag: tolerance = 0.1 - self.assertTrue(abs(mtime_after - mtime_before) < tolerance, "mtime difference greater than tolerance") + self.assertTrue( + abs(mtime_after - mtime_before) < tolerance, + "mtime difference greater than tolerance" + ) else: self.assertNotEqual(mtime_before, mtime_after) run_test("file mtime will be preserved", flag=True) run_test("file mtime will not be preserved", flag=False) - def test_save_off_spec_frames(self): # These are not defined in v2.3 and shouldn't be written. # Still make sure reading them again works and the encoding