From d21657dc0248c9f9dfebfe00f0e2d7c4e628650e Mon Sep 17 00:00:00 2001 From: Davide Romanini Date: Mon, 16 Feb 2015 13:43:21 +0100 Subject: [PATCH 01/22] Merge commit 'd933941a5ef87dc186795d55ad662611a6729ec2' as 'comicapi' --- UnRAR2/UnRARDLL/license.txt | 18 + UnRAR2/UnRARDLL/unrar.dll | Bin 0 -> 165376 bytes UnRAR2/UnRARDLL/unrar.h | 140 ++++ UnRAR2/UnRARDLL/unrar.lib | Bin 0 -> 4114 bytes UnRAR2/UnRARDLL/unrardll.txt | 606 +++++++++++++++++ UnRAR2/UnRARDLL/whatsnew.txt | 80 +++ UnRAR2/UnRARDLL/x64/readme.txt | 1 + UnRAR2/UnRARDLL/x64/unrar64.dll | Bin 0 -> 191488 bytes UnRAR2/UnRARDLL/x64/unrar64.lib | Bin 0 -> 3972 bytes UnRAR2/__init__.py | 177 +++++ UnRAR2/rar_exceptions.py | 30 + UnRAR2/test_UnRAR2.py | 138 ++++ UnRAR2/unix.py | 218 +++++++ UnRAR2/windows.py | 309 +++++++++ __init__.py | 1 + comet.py | 260 ++++++++ comicarchive.py | 1088 +++++++++++++++++++++++++++++++ comicbookinfo.py | 152 +++++ comicinfoxml.py | 293 +++++++++ filenameparser.py | 277 ++++++++ genericmetadata.py | 316 +++++++++ issuestring.py | 140 ++++ utils.py | 597 +++++++++++++++++ 23 files changed, 4841 insertions(+) create mode 100644 UnRAR2/UnRARDLL/license.txt create mode 100644 UnRAR2/UnRARDLL/unrar.dll create mode 100644 UnRAR2/UnRARDLL/unrar.h create mode 100644 UnRAR2/UnRARDLL/unrar.lib create mode 100644 UnRAR2/UnRARDLL/unrardll.txt create mode 100644 UnRAR2/UnRARDLL/whatsnew.txt create mode 100644 UnRAR2/UnRARDLL/x64/readme.txt create mode 100644 UnRAR2/UnRARDLL/x64/unrar64.dll create mode 100644 UnRAR2/UnRARDLL/x64/unrar64.lib create mode 100644 UnRAR2/__init__.py create mode 100644 UnRAR2/rar_exceptions.py create mode 100644 UnRAR2/test_UnRAR2.py create mode 100644 UnRAR2/unix.py create mode 100644 UnRAR2/windows.py create mode 100644 __init__.py create mode 100644 comet.py create mode 100644 comicarchive.py create mode 100644 comicbookinfo.py create mode 100644 comicinfoxml.py create mode 100644 filenameparser.py create mode 100644 genericmetadata.py create mode 100644 issuestring.py create mode 100644 utils.py diff --git a/UnRAR2/UnRARDLL/license.txt b/UnRAR2/UnRARDLL/license.txt new file mode 100644 index 0000000..0c1540e --- /dev/null +++ b/UnRAR2/UnRARDLL/license.txt @@ -0,0 +1,18 @@ + The unrar.dll library is freeware. This means: + + 1. All copyrights to RAR and the unrar.dll are exclusively + owned by the author - Alexander Roshal. + + 2. The unrar.dll library may be used in any software to handle RAR + archives without limitations free of charge. + + 3. THE RAR ARCHIVER AND THE UNRAR.DLL LIBRARY ARE DISTRIBUTED "AS IS". + NO WARRANTY OF ANY KIND IS EXPRESSED OR IMPLIED. YOU USE AT + YOUR OWN RISK. THE AUTHOR WILL NOT BE LIABLE FOR DATA LOSS, + DAMAGES, LOSS OF PROFITS OR ANY OTHER KIND OF LOSS WHILE USING + OR MISUSING THIS SOFTWARE. + + Thank you for your interest in RAR and unrar.dll. + + + Alexander L. Roshal \ No newline at end of file diff --git a/UnRAR2/UnRARDLL/unrar.dll b/UnRAR2/UnRARDLL/unrar.dll new file mode 100644 index 0000000000000000000000000000000000000000..9757bf3d692d8668ce892c4bee814eb5394adf37 GIT binary patch literal 165376 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 Zpd|w>8EDBsO9omp(2{|c4E$3W_-_?5`fC6H literal 0 HcmV?d00001 diff --git a/UnRAR2/UnRARDLL/unrar.h b/UnRAR2/UnRARDLL/unrar.h new file mode 100644 index 0000000..7643fa7 --- /dev/null +++ b/UnRAR2/UnRARDLL/unrar.h @@ -0,0 +1,140 @@ +#ifndef _UNRAR_DLL_ +#define _UNRAR_DLL_ + +#define ERAR_END_ARCHIVE 10 +#define ERAR_NO_MEMORY 11 +#define ERAR_BAD_DATA 12 +#define ERAR_BAD_ARCHIVE 13 +#define ERAR_UNKNOWN_FORMAT 14 +#define ERAR_EOPEN 15 +#define ERAR_ECREATE 16 +#define ERAR_ECLOSE 17 +#define ERAR_EREAD 18 +#define ERAR_EWRITE 19 +#define ERAR_SMALL_BUF 20 +#define ERAR_UNKNOWN 21 +#define ERAR_MISSING_PASSWORD 22 + +#define RAR_OM_LIST 0 +#define RAR_OM_EXTRACT 1 +#define RAR_OM_LIST_INCSPLIT 2 + +#define RAR_SKIP 0 +#define RAR_TEST 1 +#define RAR_EXTRACT 2 + +#define RAR_VOL_ASK 0 +#define RAR_VOL_NOTIFY 1 + +#define RAR_DLL_VERSION 4 + +#ifdef _UNIX +#define CALLBACK +#define PASCAL +#define LONG long +#define HANDLE void * +#define LPARAM long +#define UINT unsigned int +#endif + +struct RARHeaderData +{ + char ArcName[260]; + char FileName[260]; + unsigned int Flags; + unsigned int PackSize; + unsigned int UnpSize; + unsigned int HostOS; + unsigned int FileCRC; + unsigned int FileTime; + unsigned int UnpVer; + unsigned int Method; + unsigned int FileAttr; + char *CmtBuf; + unsigned int CmtBufSize; + unsigned int CmtSize; + unsigned int CmtState; +}; + + +struct RARHeaderDataEx +{ + char ArcName[1024]; + wchar_t ArcNameW[1024]; + char FileName[1024]; + wchar_t FileNameW[1024]; + unsigned int Flags; + unsigned int PackSize; + unsigned int PackSizeHigh; + unsigned int UnpSize; + unsigned int UnpSizeHigh; + unsigned int HostOS; + unsigned int FileCRC; + unsigned int FileTime; + unsigned int UnpVer; + unsigned int Method; + unsigned int FileAttr; + char *CmtBuf; + unsigned int CmtBufSize; + unsigned int CmtSize; + unsigned int CmtState; + unsigned int Reserved[1024]; +}; + + +struct RAROpenArchiveData +{ + char *ArcName; + unsigned int OpenMode; + unsigned int OpenResult; + char *CmtBuf; + unsigned int CmtBufSize; + unsigned int CmtSize; + unsigned int CmtState; +}; + +struct RAROpenArchiveDataEx +{ + char *ArcName; + wchar_t *ArcNameW; + unsigned int OpenMode; + unsigned int OpenResult; + char *CmtBuf; + unsigned int CmtBufSize; + unsigned int CmtSize; + unsigned int CmtState; + unsigned int Flags; + unsigned int Reserved[32]; +}; + +enum UNRARCALLBACK_MESSAGES { + UCM_CHANGEVOLUME,UCM_PROCESSDATA,UCM_NEEDPASSWORD +}; + +typedef int (CALLBACK *UNRARCALLBACK)(UINT msg,LPARAM UserData,LPARAM P1,LPARAM P2); + +typedef int (PASCAL *CHANGEVOLPROC)(char *ArcName,int Mode); +typedef int (PASCAL *PROCESSDATAPROC)(unsigned char *Addr,int Size); + +#ifdef __cplusplus +extern "C" { +#endif + +HANDLE PASCAL RAROpenArchive(struct RAROpenArchiveData *ArchiveData); +HANDLE PASCAL RAROpenArchiveEx(struct RAROpenArchiveDataEx *ArchiveData); +int PASCAL RARCloseArchive(HANDLE hArcData); +int PASCAL RARReadHeader(HANDLE hArcData,struct RARHeaderData *HeaderData); +int PASCAL RARReadHeaderEx(HANDLE hArcData,struct RARHeaderDataEx *HeaderData); +int PASCAL RARProcessFile(HANDLE hArcData,int Operation,char *DestPath,char *DestName); +int PASCAL RARProcessFileW(HANDLE hArcData,int Operation,wchar_t *DestPath,wchar_t *DestName); +void PASCAL RARSetCallback(HANDLE hArcData,UNRARCALLBACK Callback,LPARAM UserData); +void PASCAL RARSetChangeVolProc(HANDLE hArcData,CHANGEVOLPROC ChangeVolProc); +void PASCAL RARSetProcessDataProc(HANDLE hArcData,PROCESSDATAPROC ProcessDataProc); +void PASCAL RARSetPassword(HANDLE hArcData,char *Password); +int PASCAL RARGetDllVersion(); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/UnRAR2/UnRARDLL/unrar.lib b/UnRAR2/UnRARDLL/unrar.lib new file mode 100644 index 0000000000000000000000000000000000000000..0f6b3146b8ec5bd83698122653a75bcb1f2caf70 GIT binary patch literal 4114 zcmcInOK%fN5dLhFhn=T@+wLMGkQOc`amG%NR?FnI5XB_X#DU{5*aNW`;>Zr*8-HML zd*Q%x;YaK>;<|@D?B2N`{sm&IW_o&h+GA%dLQ6H>T~*y*cXd^F&DCF=PUG;`!mVPw zES9S))g_~PdnwLe5Z&davS>Xj0QdnIUjrtaK>iId^&z0?62MgW9MDV;@aYrPMAL5r znxQ$EW-URdR1?j;6I7;}XsXU++gtbdcCEU-vAMr)ZSB=}E&Ih$$LYYfcMfW`elcGA z@<3X@cd)Z-i=VOy)#?y-BcN;YV{bWMY1Xgxo+6Zmn>&E6p0KtkH%w4+SmTCs;v|hq5Q}k6xBIHyY3eY03ZFFZ zx+fc+_rUFRTkRurLD_;X896SDC@$tGFxJL_<|ObY4}6#cO4Gn+^7P&e@QLUx^$S#6 zv%o3QI~r6bs*^4S6~-}#3m8Kl1x#QPQ<%mqW{^R4pe*P6b%LAen@j2DWH4cH=}d8! z^o%ndHaMl2_XySiKZu_jIZVRQ_s9EL*FhBJx|L-3_t{EHQd}6?^`Ki%PNfL6xQkm- z4v5&=1)ztX9FY`bs!)xL7(ci^ln5Mfhx#{bsp)wlRL*)ijFpOfIck|4Hvju`dm=+` z2YEY{OsVNUe)07Be$WN(P~-QoBWe@#Yo%6`ZinmiDg@;+ReuwG6#X34CKgVGURAIu zj({&jp&s*16i>5M&r_Un$;(asj7#$q#NpYvusq+pc)!)?w7cymC&e4q&0=k9XWN%* zABt^%AWr}aV^9ds(|62oNeq~c_VZ&}XTJ9bzJ3kCSf2|oEQ@fvCfyUvISe`e#&~(T zkYlh8F(REx#9{sw{)obJ0n4JtRTew+J--3gkftw+ zK8lAdg>=Obk^Q|ACe<1m zhiOKj>CaIFCtE3_O9q#Q_9LNX1zP-xlL(Nllvu-dmg~pzpG}D|GMX{u)Gi1#<;CSh zHv&7?Qyc3?^WXOfPPS57(pXOR$RI}yJTgiSDE*ZHqx<79J5Gq5MOc0!@}1Bo1)7%K zd;?lV{?EoE`zm>VUP05+(QiN;7H@?JQOUz1Fxg7!C6zF>(qj7>?T-H(IN?vsp(PLs F{{t7Z#m)c# literal 0 HcmV?d00001 diff --git a/UnRAR2/UnRARDLL/unrardll.txt b/UnRAR2/UnRARDLL/unrardll.txt new file mode 100644 index 0000000..291c871 --- /dev/null +++ b/UnRAR2/UnRARDLL/unrardll.txt @@ -0,0 +1,606 @@ + + UnRAR.dll Manual + ~~~~~~~~~~~~~~~~ + + UnRAR.dll is a 32-bit Windows dynamic-link library which provides + file extraction from RAR archives. + + + Exported functions + +==================================================================== +HANDLE PASCAL RAROpenArchive(struct RAROpenArchiveData *ArchiveData) +==================================================================== + +Description +~~~~~~~~~~~ + Open RAR archive and allocate memory structures + +Parameters +~~~~~~~~~~ +ArchiveData Points to RAROpenArchiveData structure + +struct RAROpenArchiveData +{ + char *ArcName; + UINT OpenMode; + UINT OpenResult; + char *CmtBuf; + UINT CmtBufSize; + UINT CmtSize; + UINT CmtState; +}; + +Structure fields: + +ArcName + Input parameter which should point to zero terminated string + containing the archive name. + +OpenMode + Input parameter. + + Possible values + + RAR_OM_LIST + Open archive for reading file headers only. + + RAR_OM_EXTRACT + Open archive for testing and extracting files. + + RAR_OM_LIST_INCSPLIT + Open archive for reading file headers only. If you open an archive + in such mode, RARReadHeader[Ex] will return all file headers, + including those with "file continued from previous volume" flag. + In case of RAR_OM_LIST such headers are automatically skipped. + So if you process RAR volumes in RAR_OM_LIST_INCSPLIT mode, you will + get several file header records for same file if file is split between + volumes. For such files only the last file header record will contain + the correct file CRC and if you wish to get the correct packed size, + you need to sum up packed sizes of all parts. + +OpenResult + Output parameter. + + Possible values + + 0 Success + ERAR_NO_MEMORY Not enough memory to initialize data structures + ERAR_BAD_DATA Archive header broken + ERAR_BAD_ARCHIVE File is not valid RAR archive + ERAR_UNKNOWN_FORMAT Unknown encryption used for archive headers + ERAR_EOPEN File open error + +CmtBuf + Input parameter which should point to the buffer for archive + comments. Maximum comment size is limited to 64Kb. Comment text is + zero terminated. If the comment text is larger than the buffer + size, the comment text will be truncated. If CmtBuf is set to + NULL, comments will not be read. + +CmtBufSize + Input parameter which should contain size of buffer for archive + comments. + +CmtSize + Output parameter containing size of comments actually read into the + buffer, cannot exceed CmtBufSize. + +CmtState + Output parameter. + + Possible values + + 0 comments not present + 1 Comments read completely + ERAR_NO_MEMORY Not enough memory to extract comments + ERAR_BAD_DATA Broken comment + ERAR_UNKNOWN_FORMAT Unknown comment format + ERAR_SMALL_BUF Buffer too small, comments not completely read + +Return values +~~~~~~~~~~~~~ + Archive handle or NULL in case of error + + +======================================================================== +HANDLE PASCAL RAROpenArchiveEx(struct RAROpenArchiveDataEx *ArchiveData) +======================================================================== + +Description +~~~~~~~~~~~ + Similar to RAROpenArchive, but uses RAROpenArchiveDataEx structure + allowing to specify Unicode archive name and returning information + about archive flags. + +Parameters +~~~~~~~~~~ +ArchiveData Points to RAROpenArchiveDataEx structure + +struct RAROpenArchiveDataEx +{ + char *ArcName; + wchar_t *ArcNameW; + unsigned int OpenMode; + unsigned int OpenResult; + char *CmtBuf; + unsigned int CmtBufSize; + unsigned int CmtSize; + unsigned int CmtState; + unsigned int Flags; + unsigned int Reserved[32]; +}; + +Structure fields: + +ArcNameW + Input parameter which should point to zero terminated Unicode string + containing the archive name or NULL if Unicode name is not specified. + +Flags + Output parameter. Combination of bit flags. + + Possible values + + 0x0001 - Volume attribute (archive volume) + 0x0002 - Archive comment present + 0x0004 - Archive lock attribute + 0x0008 - Solid attribute (solid archive) + 0x0010 - New volume naming scheme ('volname.partN.rar') + 0x0020 - Authenticity information present + 0x0040 - Recovery record present + 0x0080 - Block headers are encrypted + 0x0100 - First volume (set only by RAR 3.0 and later) + +Reserved[32] + Reserved for future use. Must be zero. + +Information on other structure fields and function return values +is available above, in RAROpenArchive function description. + + +==================================================================== +int PASCAL RARCloseArchive(HANDLE hArcData) +==================================================================== + +Description +~~~~~~~~~~~ + Close RAR archive and release allocated memory. It must be called when + archive processing is finished, even if the archive processing was stopped + due to an error. + +Parameters +~~~~~~~~~~ +hArcData + This parameter should contain the archive handle obtained from the + RAROpenArchive function call. + +Return values +~~~~~~~~~~~~~ + 0 Success + ERAR_ECLOSE Archive close error + + +==================================================================== +int PASCAL RARReadHeader(HANDLE hArcData, + struct RARHeaderData *HeaderData) +==================================================================== + +Description +~~~~~~~~~~~ + Read header of file in archive. + +Parameters +~~~~~~~~~~ +hArcData + This parameter should contain the archive handle obtained from the + RAROpenArchive function call. + +HeaderData + It should point to RARHeaderData structure: + +struct RARHeaderData +{ + char ArcName[260]; + char FileName[260]; + UINT Flags; + UINT PackSize; + UINT UnpSize; + UINT HostOS; + UINT FileCRC; + UINT FileTime; + UINT UnpVer; + UINT Method; + UINT FileAttr; + char *CmtBuf; + UINT CmtBufSize; + UINT CmtSize; + UINT CmtState; +}; + +Structure fields: + +ArcName + Output parameter which contains a zero terminated string of the + current archive name. May be used to determine the current volume + name. + +FileName + Output parameter which contains a zero terminated string of the + file name in OEM (DOS) encoding. + +Flags + Output parameter which contains file flags: + + 0x01 - file continued from previous volume + 0x02 - file continued on next volume + 0x04 - file encrypted with password + 0x08 - file comment present + 0x10 - compression of previous files is used (solid flag) + + bits 7 6 5 + + 0 0 0 - dictionary size 64 Kb + 0 0 1 - dictionary size 128 Kb + 0 1 0 - dictionary size 256 Kb + 0 1 1 - dictionary size 512 Kb + 1 0 0 - dictionary size 1024 Kb + 1 0 1 - dictionary size 2048 KB + 1 1 0 - dictionary size 4096 KB + 1 1 1 - file is directory + + Other bits are reserved. + +PackSize + Output parameter means packed file size or size of the + file part if file was split between volumes. + +UnpSize + Output parameter - unpacked file size. + +HostOS + Output parameter - operating system used for archiving: + + 0 - MS DOS; + 1 - OS/2. + 2 - Win32 + 3 - Unix + +FileCRC + Output parameter which contains unpacked file CRC. In case of file parts + split between volumes only the last part contains the correct CRC + and it is accessible only in RAR_OM_LIST_INCSPLIT listing mode. + +FileTime + Output parameter - contains date and time in standard MS DOS format. + +UnpVer + Output parameter - RAR version needed to extract file. + It is encoded as 10 * Major version + minor version. + +Method + Output parameter - packing method. + +FileAttr + Output parameter - file attributes. + +CmtBuf + File comments support is not implemented in the new DLL version yet. + Now CmtState is always 0. + +/* + * Input parameter which should point to the buffer for file + * comments. Maximum comment size is limited to 64Kb. Comment text is + * a zero terminated string in OEM encoding. If the comment text is + * larger than the buffer size, the comment text will be truncated. + * If CmtBuf is set to NULL, comments will not be read. + */ + +CmtBufSize + Input parameter which should contain size of buffer for archive + comments. + +CmtSize + Output parameter containing size of comments actually read into the + buffer, should not exceed CmtBufSize. + +CmtState + Output parameter. + + Possible values + + 0 Absent comments + 1 Comments read completely + ERAR_NO_MEMORY Not enough memory to extract comments + ERAR_BAD_DATA Broken comment + ERAR_UNKNOWN_FORMAT Unknown comment format + ERAR_SMALL_BUF Buffer too small, comments not completely read + +Return values +~~~~~~~~~~~~~ + + 0 Success + ERAR_END_ARCHIVE End of archive + ERAR_BAD_DATA File header broken + + +==================================================================== +int PASCAL RARReadHeaderEx(HANDLE hArcData, + struct RARHeaderDataEx *HeaderData) +==================================================================== + +Description +~~~~~~~~~~~ + Similar to RARReadHeader, but uses RARHeaderDataEx structure, +containing information about Unicode file names and 64 bit file sizes. + +struct RARHeaderDataEx +{ + char ArcName[1024]; + wchar_t ArcNameW[1024]; + char FileName[1024]; + wchar_t FileNameW[1024]; + unsigned int Flags; + unsigned int PackSize; + unsigned int PackSizeHigh; + unsigned int UnpSize; + unsigned int UnpSizeHigh; + unsigned int HostOS; + unsigned int FileCRC; + unsigned int FileTime; + unsigned int UnpVer; + unsigned int Method; + unsigned int FileAttr; + char *CmtBuf; + unsigned int CmtBufSize; + unsigned int CmtSize; + unsigned int CmtState; + unsigned int Reserved[1024]; +}; + + +==================================================================== +int PASCAL RARProcessFile(HANDLE hArcData, + int Operation, + char *DestPath, + char *DestName) +==================================================================== + +Description +~~~~~~~~~~~ + Performs action and moves the current position in the archive to + the next file. Extract or test the current file from the archive + opened in RAR_OM_EXTRACT mode. If the mode RAR_OM_LIST is set, + then a call to this function will simply skip the archive position + to the next file. + +Parameters +~~~~~~~~~~ +hArcData + This parameter should contain the archive handle obtained from the + RAROpenArchive function call. + +Operation + File operation. + + Possible values + + RAR_SKIP Move to the next file in the archive. If the + archive is solid and RAR_OM_EXTRACT mode was set + when the archive was opened, the current file will + be processed - the operation will be performed + slower than a simple seek. + + RAR_TEST Test the current file and move to the next file in + the archive. If the archive was opened with + RAR_OM_LIST mode, the operation is equal to + RAR_SKIP. + + RAR_EXTRACT Extract the current file and move to the next file. + If the archive was opened with RAR_OM_LIST mode, + the operation is equal to RAR_SKIP. + + +DestPath + This parameter should point to a zero terminated string containing the + destination directory to which to extract files to. If DestPath is equal + to NULL, it means extract to the current directory. This parameter has + meaning only if DestName is NULL. + +DestName + This parameter should point to a string containing the full path and name + to assign to extracted file or it can be NULL to use the default name. + If DestName is defined (not NULL), it overrides both the original file + name saved in the archive and path specigied in DestPath setting. + + Both DestPath and DestName must be in OEM encoding. If necessary, + use CharToOem to convert text to OEM before passing to this function. + +Return values +~~~~~~~~~~~~~ + 0 Success + ERAR_BAD_DATA File CRC error + ERAR_BAD_ARCHIVE Volume is not valid RAR archive + ERAR_UNKNOWN_FORMAT Unknown archive format + ERAR_EOPEN Volume open error + ERAR_ECREATE File create error + ERAR_ECLOSE File close error + ERAR_EREAD Read error + ERAR_EWRITE Write error + + +Note: if you wish to cancel extraction, return -1 when processing + UCM_PROCESSDATA callback message. + + +==================================================================== +int PASCAL RARProcessFileW(HANDLE hArcData, + int Operation, + wchar_t *DestPath, + wchar_t *DestName) +==================================================================== + +Description +~~~~~~~~~~~ + Unicode version of RARProcessFile. It uses Unicode DestPath + and DestName parameters, other parameters and return values + are the same as in RARProcessFile. + + +==================================================================== +void PASCAL RARSetCallback(HANDLE hArcData, + int PASCAL (*CallbackProc)(UINT msg,LPARAM UserData,LPARAM P1,LPARAM P2), + LPARAM UserData); +==================================================================== + +Description +~~~~~~~~~~~ + Set a user-defined callback function to process Unrar events. + +Parameters +~~~~~~~~~~ +hArcData + This parameter should contain the archive handle obtained from the + RAROpenArchive function call. + +CallbackProc + It should point to a user-defined callback function. + + The function will be passed four parameters: + + + msg Type of event. Described below. + + UserData User defined value passed to RARSetCallback. + + P1 and P2 Event dependent parameters. Described below. + + + Possible events + + UCM_CHANGEVOLUME Process volume change. + + P1 Points to the zero terminated name + of the next volume. + + P2 The function call mode: + + RAR_VOL_ASK Required volume is absent. The function should + prompt user and return a positive value + to retry or return -1 value to terminate + operation. The function may also specify a new + volume name, placing it to the address specified + by P1 parameter. + + RAR_VOL_NOTIFY Required volume is successfully opened. + This is a notification call and volume name + modification is not allowed. The function should + return a positive value to continue or -1 + to terminate operation. + + UCM_PROCESSDATA Process unpacked data. It may be used to read + a file while it is being extracted or tested + without actual extracting file to disk. + Return a positive value to continue process + or -1 to cancel the archive operation + + P1 Address pointing to the unpacked data. + Function may refer to the data but must not + change it. + + P2 Size of the unpacked data. It is guaranteed + only that the size will not exceed the maximum + dictionary size (4 Mb in RAR 3.0). + + UCM_NEEDPASSWORD DLL needs a password to process archive. + This message must be processed if you wish + to be able to handle archives with encrypted + file names. It can be also used as replacement + of RARSetPassword function even for usual + encrypted files with non-encrypted names. + + P1 Address pointing to the buffer for a password. + You need to copy a password here. + + P2 Size of the password buffer. + + +UserData + User data passed to callback function. + + Other functions of UnRAR.dll should not be called from the callback + function. + +Return values +~~~~~~~~~~~~~ + None + + + +==================================================================== +void PASCAL RARSetChangeVolProc(HANDLE hArcData, + int PASCAL (*ChangeVolProc)(char *ArcName,int Mode)); +==================================================================== + +Obsoleted, use RARSetCallback instead. + + + +==================================================================== +void PASCAL RARSetProcessDataProc(HANDLE hArcData, + int PASCAL (*ProcessDataProc)(unsigned char *Addr,int Size)) +==================================================================== + +Obsoleted, use RARSetCallback instead. + + +==================================================================== +void PASCAL RARSetPassword(HANDLE hArcData, + char *Password); +==================================================================== + +Description +~~~~~~~~~~~ + Set a password to decrypt files. + +Parameters +~~~~~~~~~~ +hArcData + This parameter should contain the archive handle obtained from the + RAROpenArchive function call. + +Password + It should point to a string containing a zero terminated password. + +Return values +~~~~~~~~~~~~~ + None + + +==================================================================== +void PASCAL RARGetDllVersion(); +==================================================================== + +Description +~~~~~~~~~~~ + Returns API version. + +Parameters +~~~~~~~~~~ + None. + +Return values +~~~~~~~~~~~~~ + Returns an integer value denoting UnRAR.dll API version, which is also +defined in unrar.h as RAR_DLL_VERSION. API version number is incremented +only in case of noticeable changes in UnRAR.dll API. Do not confuse it +with version of UnRAR.dll stored in DLL resources, which is incremented +with every DLL rebuild. + + If RARGetDllVersion() returns a value lower than UnRAR.dll which your +application was designed for, it may indicate that DLL version is too old +and it will fail to provide all necessary functions to your application. + + This function is absent in old versions of UnRAR.dll, so it is safer +to use LoadLibrary and GetProcAddress to access this function. + diff --git a/UnRAR2/UnRARDLL/whatsnew.txt b/UnRAR2/UnRARDLL/whatsnew.txt new file mode 100644 index 0000000..84ad72c --- /dev/null +++ b/UnRAR2/UnRARDLL/whatsnew.txt @@ -0,0 +1,80 @@ +List of unrar.dll API changes. We do not include performance and reliability +improvements into this list, but this library and RAR/UnRAR tools share +the same source code. So the latest version of unrar.dll usually contains +same decompression algorithm changes as the latest UnRAR version. +============================================================================ + +-- 18 January 2008 + +all LONG parameters of CallbackProc function were changed +to LPARAM type for 64 bit mode compatibility. + + +-- 12 December 2007 + +Added new RAR_OM_LIST_INCSPLIT open mode for function RAROpenArchive. + + +-- 14 August 2007 + +Added NoCrypt\unrar_nocrypt.dll without decryption code for those +applications where presence of encryption or decryption code is not +allowed because of legal restrictions. + + +-- 14 December 2006 + +Added ERAR_MISSING_PASSWORD error type. This error is returned +if empty password is specified for encrypted file. + + +-- 12 June 2003 + +Added RARProcessFileW function, Unicode version of RARProcessFile + + +-- 9 August 2002 + +Added RAROpenArchiveEx function allowing to specify Unicode archive +name and get archive flags. + + +-- 24 January 2002 + +Added RARReadHeaderEx function allowing to read Unicode file names +and 64 bit file sizes. + + +-- 23 January 2002 + +Added ERAR_UNKNOWN error type (it is used for all errors which +do not have special ERAR code yet) and UCM_NEEDPASSWORD callback +message. + +Unrar.dll automatically opens all next volumes not only when extracting, +but also in RAR_OM_LIST mode. + + +-- 27 November 2001 + +RARSetChangeVolProc and RARSetProcessDataProc are replaced by +the single callback function installed with RARSetCallback. +Unlike old style callbacks, the new function accepts the user defined +parameter. Unrar.dll still supports RARSetChangeVolProc and +RARSetProcessDataProc for compatibility purposes, but if you write +a new application, better use RARSetCallback. + +File comments support is not implemented in the new DLL version yet. +Now CmtState is always 0. + + +-- 13 August 2001 + +Added RARGetDllVersion function, so you may distinguish old unrar.dll, +which used C style callback functions and the new one with PASCAL callbacks. + + +-- 10 May 2001 + +Callback functions in RARSetChangeVolProc and RARSetProcessDataProc +use PASCAL style call convention now. diff --git a/UnRAR2/UnRARDLL/x64/readme.txt b/UnRAR2/UnRARDLL/x64/readme.txt new file mode 100644 index 0000000..8f3b4e1 --- /dev/null +++ b/UnRAR2/UnRARDLL/x64/readme.txt @@ -0,0 +1 @@ +This is x64 version of unrar.dll. diff --git a/UnRAR2/UnRARDLL/x64/unrar64.dll b/UnRAR2/UnRARDLL/x64/unrar64.dll new file mode 100644 index 0000000000000000000000000000000000000000..e17a19e59113c2bd17d1db85f594a1f7c8d4707f GIT binary patch literal 191488 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 literal 0 HcmV?d00001 diff --git a/UnRAR2/UnRARDLL/x64/unrar64.lib b/UnRAR2/UnRARDLL/x64/unrar64.lib new file mode 100644 index 0000000000000000000000000000000000000000..fd037919ee10c91665fa5f30e9ec6f37ac9e03c7 GIT binary patch literal 3972 zcmcImJ5L)y5FTt^e(;c!Dj|{+Ath-DG3N`GqR0l5VDYldtHT_=3$}9D$TkUGIz*8! zb;?lEr=v!Sl){l{Qsh6-lUeWH-K@_T8+R=JFX0GJG)aPil0o?14WT1u~E*0RaD)!9rcy|$9c9u&)3SydpLU0z>YbQ{2D zVZq1O=GT`OvdP3+0y2rr%Dz!dU>FJ?4V<8`x1ViL%H6rcrdFs%yIHeTFtp!L@t7EYzHkb;~448R}+APB=S1U=xncFgNykIP$BP^!vZHz9Cb1ex(;lg;n&NAB^TZ`y9008Qv_ zur8+{+2dPer=4|DqS#t_cWpHdMl&VN91;nKHAG2Lg9OQ*)zQ&OX~1k#rj&Alj@ZGI zlno)g_N)?T*o1DHb&B6~N-MD``;^v?ypXDWTL|<3GLn}G-Pk$U9bXk)NMPfO)e&_Z zMrS4pI;QY=#2UJfjh`rgg~E$<9460%9iHC2V2Z#Mie<;NaR{Gzunu8%uo56nJ%xLt zn0D~1K7w}%*1p5gQXy9^RZ8!x;b)oW;l@3K>vR4ml53`RbY<7T{=&{NJ z8T{VurRFE@w#Af4)VkTX{rZuo&XgbZ#jzyD7qIBL_XE5H6A0N>>1QEQQ3Q7cd^KUsd!5{I~zUy7w?cE7ETsF~fJN4MR` z-rdP%UC3efzGO+NAFDqoRO#VZ@&!b}I?go3eo@Pw zv3G-qqtSb&xN%qfe|O$=y&Y@eo8QK> Y-(qC?Tll6l_w5P#7I3f{o_G}UA258WCIA2c literal 0 HcmV?d00001 diff --git a/UnRAR2/__init__.py b/UnRAR2/__init__.py new file mode 100644 index 0000000..a913fcb --- /dev/null +++ b/UnRAR2/__init__.py @@ -0,0 +1,177 @@ +# Copyright (c) 2003-2005 Jimmy Retzlaff, 2008 Konstantin Yegupov +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +""" +pyUnRAR2 is a ctypes based wrapper around the free UnRAR.dll. + +It is an modified version of Jimmy Retzlaff's pyUnRAR - more simple, +stable and foolproof. +Notice that it has INCOMPATIBLE interface. + +It enables reading and unpacking of archives created with the +RAR/WinRAR archivers. There is a low-level interface which is very +similar to the C interface provided by UnRAR. There is also a +higher level interface which makes some common operations easier. +""" + +__version__ = '0.99.3' + +try: + WindowsError + in_windows = True +except NameError: + in_windows = False + +if in_windows: + from windows import RarFileImplementation +else: + from unix import RarFileImplementation + + +import fnmatch, time, weakref + +class RarInfo(object): + """Represents a file header in an archive. Don't instantiate directly. + Use only to obtain information about file. + YOU CANNOT EXTRACT FILE CONTENTS USING THIS OBJECT. + USE METHODS OF RarFile CLASS INSTEAD. + + Properties: + index - index of file within the archive + filename - name of the file in the archive including path (if any) + datetime - file date/time as a struct_time suitable for time.strftime + isdir - True if the file is a directory + size - size in bytes of the uncompressed file + comment - comment associated with the file + + Note - this is not currently intended to be a Python file-like object. + """ + + def __init__(self, rarfile, data): + self.rarfile = weakref.proxy(rarfile) + self.index = data['index'] + self.filename = data['filename'] + self.isdir = data['isdir'] + self.size = data['size'] + self.datetime = data['datetime'] + self.comment = data['comment'] + + + + def __str__(self): + try : + arcName = self.rarfile.archiveName + except ReferenceError: + arcName = "[ARCHIVE_NO_LONGER_LOADED]" + return '' % (self.filename, arcName) + +class RarFile(RarFileImplementation): + + def __init__(self, archiveName, password=None): + """Instantiate the archive. + + archiveName is the name of the RAR file. + password is used to decrypt the files in the archive. + + Properties: + comment - comment associated with the archive + + >>> print RarFile('test.rar').comment + This is a test. + """ + self.archiveName = archiveName + RarFileImplementation.init(self, password) + + def __del__(self): + self.destruct() + + def infoiter(self): + """Iterate over all the files in the archive, generating RarInfos. + + >>> import os + >>> for fileInArchive in RarFile('test.rar').infoiter(): + ... print os.path.split(fileInArchive.filename)[-1], + ... print fileInArchive.isdir, + ... print fileInArchive.size, + ... print fileInArchive.comment, + ... print tuple(fileInArchive.datetime)[0:5], + ... print time.strftime('%a, %d %b %Y %H:%M', fileInArchive.datetime) + test True 0 None (2003, 6, 30, 1, 59) Mon, 30 Jun 2003 01:59 + test.txt False 20 None (2003, 6, 30, 2, 1) Mon, 30 Jun 2003 02:01 + this.py False 1030 None (2002, 2, 8, 16, 47) Fri, 08 Feb 2002 16:47 + """ + for params in RarFileImplementation.infoiter(self): + yield RarInfo(self, params) + + def infolist(self): + """Return a list of RarInfos, descripting the contents of the archive.""" + return list(self.infoiter()) + + def read_files(self, condition='*'): + """Read specific files from archive into memory. + If "condition" is a list of numbers, then return files which have those positions in infolist. + If "condition" is a string, then it is treated as a wildcard for names of files to extract. + If "condition" is a function, it is treated as a callback function, which accepts a RarInfo object + and returns boolean True (extract) or False (skip). + If "condition" is omitted, all files are returned. + + Returns list of tuples (RarInfo info, str contents) + """ + checker = condition2checker(condition) + return RarFileImplementation.read_files(self, checker) + + + def extract(self, condition='*', path='.', withSubpath=True, overwrite=True): + """Extract specific files from archive to disk. + + If "condition" is a list of numbers, then extract files which have those positions in infolist. + If "condition" is a string, then it is treated as a wildcard for names of files to extract. + If "condition" is a function, it is treated as a callback function, which accepts a RarInfo object + and returns either boolean True (extract) or boolean False (skip). + DEPRECATED: If "condition" callback returns string (only supported for Windows) - + that string will be used as a new name to save the file under. + If "condition" is omitted, all files are extracted. + + "path" is a directory to extract to + "withSubpath" flag denotes whether files are extracted with their full path in the archive. + "overwrite" flag denotes whether extracted files will overwrite old ones. Defaults to true. + + Returns list of RarInfos for extracted files.""" + checker = condition2checker(condition) + return RarFileImplementation.extract(self, checker, path, withSubpath, overwrite) + +def condition2checker(condition): + """Converts different condition types to callback""" + if type(condition) in [str, unicode]: + def smatcher(info): + return fnmatch.fnmatch(info.filename, condition) + return smatcher + elif type(condition) in [list, tuple] and type(condition[0]) in [int, long]: + def imatcher(info): + return info.index in condition + return imatcher + elif callable(condition): + return condition + else: + raise TypeError + + diff --git a/UnRAR2/rar_exceptions.py b/UnRAR2/rar_exceptions.py new file mode 100644 index 0000000..d90d1c8 --- /dev/null +++ b/UnRAR2/rar_exceptions.py @@ -0,0 +1,30 @@ +# Copyright (c) 2003-2005 Jimmy Retzlaff, 2008 Konstantin Yegupov +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +# Low level interface - see UnRARDLL\UNRARDLL.TXT + + +class ArchiveHeaderBroken(Exception): pass +class InvalidRARArchive(Exception): pass +class FileOpenError(Exception): pass +class IncorrectRARPassword(Exception): pass +class InvalidRARArchiveUsage(Exception): pass diff --git a/UnRAR2/test_UnRAR2.py b/UnRAR2/test_UnRAR2.py new file mode 100644 index 0000000..e86ba2c --- /dev/null +++ b/UnRAR2/test_UnRAR2.py @@ -0,0 +1,138 @@ +import os, sys + +import UnRAR2 +from UnRAR2.rar_exceptions import * + + +def cleanup(dir='test'): + for path, dirs, files in os.walk(dir): + for fn in files: + os.remove(os.path.join(path, fn)) + for dir in dirs: + os.removedirs(os.path.join(path, dir)) + + +# basic test +cleanup() +rarc = UnRAR2.RarFile('test.rar') +rarc.infolist() +assert rarc.comment == "This is a test." +for info in rarc.infoiter(): + saveinfo = info + assert (str(info)=="""""") + break +rarc.extract() +assert os.path.exists('test'+os.sep+'test.txt') +assert os.path.exists('test'+os.sep+'this.py') +del rarc +assert (str(saveinfo)=="""""") +cleanup() + +# extract all the files in test.rar +cleanup() +UnRAR2.RarFile('test.rar').extract() +assert os.path.exists('test'+os.sep+'test.txt') +assert os.path.exists('test'+os.sep+'this.py') +cleanup() + +# extract all the files in test.rar matching the wildcard *.txt +cleanup() +UnRAR2.RarFile('test.rar').extract('*.txt') +assert os.path.exists('test'+os.sep+'test.txt') +assert not os.path.exists('test'+os.sep+'this.py') +cleanup() + + +# check the name and size of each file, extracting small ones +cleanup() +archive = UnRAR2.RarFile('test.rar') +assert archive.comment == 'This is a test.' +archive.extract(lambda rarinfo: rarinfo.size <= 1024) +for rarinfo in archive.infoiter(): + if rarinfo.size <= 1024 and not rarinfo.isdir: + assert rarinfo.size == os.stat(rarinfo.filename).st_size +assert file('test'+os.sep+'test.txt', 'rt').read() == 'This is only a test.' +assert not os.path.exists('test'+os.sep+'this.py') +cleanup() + + +# extract this.py, overriding it's destination +cleanup('test2') +archive = UnRAR2.RarFile('test.rar') +archive.extract('*.py', 'test2', False) +assert os.path.exists('test2'+os.sep+'this.py') +cleanup('test2') + + +# extract test.txt to memory +cleanup() +archive = UnRAR2.RarFile('test.rar') +entries = UnRAR2.RarFile('test.rar').read_files('*test.txt') +assert len(entries)==1 +assert entries[0][0].filename.endswith('test.txt') +assert entries[0][1]=='This is only a test.' + + +# extract all the files in test.rar with overwriting +cleanup() +fo = open('test'+os.sep+'test.txt',"wt") +fo.write("blah") +fo.close() +UnRAR2.RarFile('test.rar').extract('*.txt') +assert open('test'+os.sep+'test.txt',"rt").read()!="blah" +cleanup() + +# extract all the files in test.rar without overwriting +cleanup() +fo = open('test'+os.sep+'test.txt',"wt") +fo.write("blahblah") +fo.close() +UnRAR2.RarFile('test.rar').extract('*.txt', overwrite = False) +assert open('test'+os.sep+'test.txt',"rt").read()=="blahblah" +cleanup() + +# list big file in an archive +list(UnRAR2.RarFile('test_nulls.rar').infoiter()) + +# extract files from an archive with protected files +cleanup() +rarc = UnRAR2.RarFile('test_protected_files.rar', password="protected") +rarc.extract() +assert os.path.exists('test'+os.sep+'top_secret_xxx_file.txt') +cleanup() +errored = False +try: + UnRAR2.RarFile('test_protected_files.rar', password="proteqted").extract() +except IncorrectRARPassword: + errored = True +assert not os.path.exists('test'+os.sep+'top_secret_xxx_file.txt') +assert errored +cleanup() + +# extract files from an archive with protected headers +cleanup() +UnRAR2.RarFile('test_protected_headers.rar', password="secret").extract() +assert os.path.exists('test'+os.sep+'top_secret_xxx_file.txt') +cleanup() +errored = False +try: + UnRAR2.RarFile('test_protected_headers.rar', password="seqret").extract() +except IncorrectRARPassword: + errored = True +assert not os.path.exists('test'+os.sep+'top_secret_xxx_file.txt') +assert errored +cleanup() + +# make sure docstring examples are working +import doctest +doctest.testmod(UnRAR2) + +# update documentation +import pydoc +pydoc.writedoc(UnRAR2) + +# cleanup +try: + os.remove('__init__.pyc') +except: + pass diff --git a/UnRAR2/unix.py b/UnRAR2/unix.py new file mode 100644 index 0000000..bd9ee85 --- /dev/null +++ b/UnRAR2/unix.py @@ -0,0 +1,218 @@ +# Copyright (c) 2003-2005 Jimmy Retzlaff, 2008 Konstantin Yegupov +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +# Unix version uses unrar command line executable + +import subprocess +import gc + +import os, os.path +import time, re + +from rar_exceptions import * + +class UnpackerNotInstalled(Exception): pass + +rar_executable_cached = None +rar_executable_version = None + +def call_unrar(params): + "Calls rar/unrar command line executable, returns stdout pipe" + global rar_executable_cached + if rar_executable_cached is None: + for command in ('unrar', 'rar'): + try: + subprocess.Popen([command], stdout=subprocess.PIPE) + rar_executable_cached = command + break + except OSError: + pass + if rar_executable_cached is None: + raise UnpackerNotInstalled("No suitable RAR unpacker installed") + + assert type(params) == list, "params must be list" + args = [rar_executable_cached] + params + try: + gc.disable() # See http://bugs.python.org/issue1336 + return subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + finally: + gc.enable() + +class RarFileImplementation(object): + + def init(self, password=None): + global rar_executable_version + self.password = password + + + stdoutdata, stderrdata = self.call('v', []).communicate() + + for line in stderrdata.splitlines(): + if line.strip().startswith("Cannot open"): + raise FileOpenError + if line.find("CRC failed")>=0: + raise IncorrectRARPassword + accum = [] + source = iter(stdoutdata.splitlines()) + line = '' + while not (line.startswith('UNRAR')): + line = source.next() + signature = line + # The code below is mighty flaky + # and will probably crash on localized versions of RAR + # but I see no safe way to rewrite it using a CLI tool + if signature.startswith("UNRAR 4"): + rar_executable_version = 4 + while not (line.startswith('Comment:') or line.startswith('Pathname/Comment')): + if line.strip().endswith('is not RAR archive'): + raise InvalidRARArchive + line = source.next() + while not line.startswith('Pathname/Comment'): + accum.append(line.rstrip('\n')) + line = source.next() + if len(accum): + accum[0] = accum[0][9:] # strip out "Comment:" part + self.comment = '\n'.join(accum[:-1]) + else: + self.comment = None + elif signature.startswith("UNRAR 5"): + rar_executable_version = 5 + line = source.next() + while not line.startswith('Archive:'): + if line.strip().endswith('is not RAR archive'): + raise InvalidRARArchive + accum.append(line.rstrip('\n')) + line = source.next() + if len(accum): + self.comment = '\n'.join(accum[:-1]).strip() + else: + self.comment = None + else: + raise UnpackerNotInstalled("Unsupported RAR version, expected 4.x or 5.x, found: " + + signature.split(" ")[1]) + + + def escaped_password(self): + return '-' if self.password == None else self.password + + + def call(self, cmd, options=[], files=[]): + options2 = options + ['p'+self.escaped_password()] + soptions = ['-'+x for x in options2] + return call_unrar([cmd]+soptions+['--',self.archiveName]+files) + + def infoiter(self): + + command = "v" if rar_executable_version == 4 else "l" + stdoutdata, stderrdata = self.call(command, ['c-']).communicate() + + for line in stderrdata.splitlines(): + if line.strip().startswith("Cannot open"): + raise FileOpenError + + accum = [] + source = iter(stdoutdata.splitlines()) + line = '' + while not line.startswith('-----------'): + if line.strip().endswith('is not RAR archive'): + raise InvalidRARArchive + if line.startswith("CRC failed") or line.startswith("Checksum error"): + raise IncorrectRARPassword + line = source.next() + line = source.next() + i = 0 + re_spaces = re.compile(r"\s+") + if rar_executable_version == 4: + while not line.startswith('-----------'): + accum.append(line) + if len(accum)==2: + data = {} + data['index'] = i + # asterisks mark password-encrypted files + data['filename'] = accum[0].strip().lstrip("*") # asterisks marks password-encrypted files + fields = re_spaces.split(accum[1].strip()) + data['size'] = int(fields[0]) + attr = fields[5] + data['isdir'] = 'd' in attr.lower() + data['datetime'] = time.strptime(fields[3]+" "+fields[4], '%d-%m-%y %H:%M') + data['comment'] = None + yield data + accum = [] + i += 1 + line = source.next() + elif rar_executable_version == 5: + while not line.startswith('-----------'): + fields = line.strip().lstrip("*").split() + data = {} + data['index'] = i + data['filename'] = " ".join(fields[4:]) + data['size'] = int(fields[1]) + attr = fields[0] + data['isdir'] = 'd' in attr.lower() + data['datetime'] = time.strptime(fields[2]+" "+fields[3], '%d-%m-%y %H:%M') + data['comment'] = None + yield data + i += 1 + line = source.next() + + + def read_files(self, checker): + res = [] + for info in self.infoiter(): + checkres = checker(info) + if checkres==True and not info.isdir: + pipe = self.call('p', ['inul'], [info.filename]).stdout + res.append((info, pipe.read())) + return res + + + def extract(self, checker, path, withSubpath, overwrite): + res = [] + command = 'x' + if not withSubpath: + command = 'e' + options = [] + if overwrite: + options.append('o+') + else: + options.append('o-') + if not path.endswith(os.sep): + path += os.sep + names = [] + for info in self.infoiter(): + checkres = checker(info) + if type(checkres) in [str, unicode]: + raise NotImplementedError("Condition callbacks returning strings are deprecated and only supported in Windows") + if checkres==True and not info.isdir: + names.append(info.filename) + res.append(info) + names.append(path) + proc = self.call(command, options, names) + stdoutdata, stderrdata = proc.communicate() + if stderrdata.find("CRC failed")>=0 or stderrdata.find("Checksum error")>=0: + raise IncorrectRARPassword + return res + + def destruct(self): + pass + + diff --git a/UnRAR2/windows.py b/UnRAR2/windows.py new file mode 100644 index 0000000..bb92481 --- /dev/null +++ b/UnRAR2/windows.py @@ -0,0 +1,309 @@ +# Copyright (c) 2003-2005 Jimmy Retzlaff, 2008 Konstantin Yegupov +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +# Low level interface - see UnRARDLL\UNRARDLL.TXT + +from __future__ import generators + +import ctypes, ctypes.wintypes +import os, os.path, sys +import Queue +import time + +from rar_exceptions import * + +ERAR_END_ARCHIVE = 10 +ERAR_NO_MEMORY = 11 +ERAR_BAD_DATA = 12 +ERAR_BAD_ARCHIVE = 13 +ERAR_UNKNOWN_FORMAT = 14 +ERAR_EOPEN = 15 +ERAR_ECREATE = 16 +ERAR_ECLOSE = 17 +ERAR_EREAD = 18 +ERAR_EWRITE = 19 +ERAR_SMALL_BUF = 20 +ERAR_UNKNOWN = 21 + +RAR_OM_LIST = 0 +RAR_OM_EXTRACT = 1 + +RAR_SKIP = 0 +RAR_TEST = 1 +RAR_EXTRACT = 2 + +RAR_VOL_ASK = 0 +RAR_VOL_NOTIFY = 1 + +RAR_DLL_VERSION = 3 + +# enum UNRARCALLBACK_MESSAGES +UCM_CHANGEVOLUME = 0 +UCM_PROCESSDATA = 1 +UCM_NEEDPASSWORD = 2 + +architecture_bits = ctypes.sizeof(ctypes.c_voidp)*8 +dll_name = "unrar.dll" +if architecture_bits == 64: + dll_name = "x64\\unrar64.dll" + + +try: + unrar = ctypes.WinDLL(os.path.join(os.path.split(__file__)[0], 'UnRARDLL', dll_name)) +except WindowsError: + unrar = ctypes.WinDLL(dll_name) + + +class RAROpenArchiveDataEx(ctypes.Structure): + def __init__(self, ArcName=None, ArcNameW=u'', OpenMode=RAR_OM_LIST): + self.CmtBuf = ctypes.c_buffer(64*1024) + ctypes.Structure.__init__(self, ArcName=ArcName, ArcNameW=ArcNameW, OpenMode=OpenMode, _CmtBuf=ctypes.addressof(self.CmtBuf), CmtBufSize=ctypes.sizeof(self.CmtBuf)) + + _fields_ = [ + ('ArcName', ctypes.c_char_p), + ('ArcNameW', ctypes.c_wchar_p), + ('OpenMode', ctypes.c_uint), + ('OpenResult', ctypes.c_uint), + ('_CmtBuf', ctypes.c_voidp), + ('CmtBufSize', ctypes.c_uint), + ('CmtSize', ctypes.c_uint), + ('CmtState', ctypes.c_uint), + ('Flags', ctypes.c_uint), + ('Reserved', ctypes.c_uint*32), + ] + +class RARHeaderDataEx(ctypes.Structure): + def __init__(self): + self.CmtBuf = ctypes.c_buffer(64*1024) + ctypes.Structure.__init__(self, _CmtBuf=ctypes.addressof(self.CmtBuf), CmtBufSize=ctypes.sizeof(self.CmtBuf)) + + _fields_ = [ + ('ArcName', ctypes.c_char*1024), + ('ArcNameW', ctypes.c_wchar*1024), + ('FileName', ctypes.c_char*1024), + ('FileNameW', ctypes.c_wchar*1024), + ('Flags', ctypes.c_uint), + ('PackSize', ctypes.c_uint), + ('PackSizeHigh', ctypes.c_uint), + ('UnpSize', ctypes.c_uint), + ('UnpSizeHigh', ctypes.c_uint), + ('HostOS', ctypes.c_uint), + ('FileCRC', ctypes.c_uint), + ('FileTime', ctypes.c_uint), + ('UnpVer', ctypes.c_uint), + ('Method', ctypes.c_uint), + ('FileAttr', ctypes.c_uint), + ('_CmtBuf', ctypes.c_voidp), + ('CmtBufSize', ctypes.c_uint), + ('CmtSize', ctypes.c_uint), + ('CmtState', ctypes.c_uint), + ('Reserved', ctypes.c_uint*1024), + ] + +def DosDateTimeToTimeTuple(dosDateTime): + """Convert an MS-DOS format date time to a Python time tuple. + """ + dosDate = dosDateTime >> 16 + dosTime = dosDateTime & 0xffff + day = dosDate & 0x1f + month = (dosDate >> 5) & 0xf + year = 1980 + (dosDate >> 9) + second = 2*(dosTime & 0x1f) + minute = (dosTime >> 5) & 0x3f + hour = dosTime >> 11 + return time.localtime(time.mktime((year, month, day, hour, minute, second, 0, 1, -1))) + +def _wrap(restype, function, argtypes): + result = function + result.argtypes = argtypes + result.restype = restype + return result + +RARGetDllVersion = _wrap(ctypes.c_int, unrar.RARGetDllVersion, []) + +RAROpenArchiveEx = _wrap(ctypes.wintypes.HANDLE, unrar.RAROpenArchiveEx, [ctypes.POINTER(RAROpenArchiveDataEx)]) + +RARReadHeaderEx = _wrap(ctypes.c_int, unrar.RARReadHeaderEx, [ctypes.wintypes.HANDLE, ctypes.POINTER(RARHeaderDataEx)]) + +_RARSetPassword = _wrap(ctypes.c_int, unrar.RARSetPassword, [ctypes.wintypes.HANDLE, ctypes.c_char_p]) +def RARSetPassword(*args, **kwargs): + _RARSetPassword(*args, **kwargs) + +RARProcessFile = _wrap(ctypes.c_int, unrar.RARProcessFile, [ctypes.wintypes.HANDLE, ctypes.c_int, ctypes.c_char_p, ctypes.c_char_p]) + +RARCloseArchive = _wrap(ctypes.c_int, unrar.RARCloseArchive, [ctypes.wintypes.HANDLE]) + +UNRARCALLBACK = ctypes.WINFUNCTYPE(ctypes.c_int, ctypes.c_uint, ctypes.c_long, ctypes.c_long, ctypes.c_long) +RARSetCallback = _wrap(ctypes.c_int, unrar.RARSetCallback, [ctypes.wintypes.HANDLE, UNRARCALLBACK, ctypes.c_long]) + + + +RARExceptions = { + ERAR_NO_MEMORY : MemoryError, + ERAR_BAD_DATA : ArchiveHeaderBroken, + ERAR_BAD_ARCHIVE : InvalidRARArchive, + ERAR_EOPEN : FileOpenError, + } + +class PassiveReader: + """Used for reading files to memory""" + def __init__(self, usercallback = None): + self.buf = [] + self.ucb = usercallback + + def _callback(self, msg, UserData, P1, P2): + if msg == UCM_PROCESSDATA: + data = (ctypes.c_char*P2).from_address(P1).raw + if self.ucb!=None: + self.ucb(data) + else: + self.buf.append(data) + return 1 + + def get_result(self): + return ''.join(self.buf) + +class RarInfoIterator(object): + def __init__(self, arc): + self.arc = arc + self.index = 0 + self.headerData = RARHeaderDataEx() + self.res = RARReadHeaderEx(self.arc._handle, ctypes.byref(self.headerData)) + if self.res==ERAR_BAD_DATA: + raise IncorrectRARPassword + self.arc.lockStatus = "locked" + self.arc.needskip = False + + def __iter__(self): + return self + + def next(self): + if self.index>0: + if self.arc.needskip: + RARProcessFile(self.arc._handle, RAR_SKIP, None, None) + self.res = RARReadHeaderEx(self.arc._handle, ctypes.byref(self.headerData)) + + if self.res: + raise StopIteration + self.arc.needskip = True + + data = {} + data['index'] = self.index + data['filename'] = self.headerData.FileName + data['datetime'] = DosDateTimeToTimeTuple(self.headerData.FileTime) + data['isdir'] = ((self.headerData.Flags & 0xE0) == 0xE0) + data['size'] = self.headerData.UnpSize + (self.headerData.UnpSizeHigh << 32) + if self.headerData.CmtState == 1: + data['comment'] = self.headerData.CmtBuf.value + else: + data['comment'] = None + self.index += 1 + return data + + + def __del__(self): + self.arc.lockStatus = "finished" + +def generate_password_provider(password): + def password_provider_callback(msg, UserData, P1, P2): + if msg == UCM_NEEDPASSWORD and password!=None: + (ctypes.c_char*P2).from_address(P1).value = password + return 1 + return password_provider_callback + +class RarFileImplementation(object): + + def init(self, password=None): + self.password = password + archiveData = RAROpenArchiveDataEx(ArcNameW=self.archiveName, OpenMode=RAR_OM_EXTRACT) + self._handle = RAROpenArchiveEx(ctypes.byref(archiveData)) + self.c_callback = UNRARCALLBACK(generate_password_provider(self.password)) + RARSetCallback(self._handle, self.c_callback, 1) + + if archiveData.OpenResult != 0: + raise RARExceptions[archiveData.OpenResult] + + if archiveData.CmtState == 1: + self.comment = archiveData.CmtBuf.value + else: + self.comment = None + + if password: + RARSetPassword(self._handle, password) + + self.lockStatus = "ready" + + + + def destruct(self): + if self._handle and RARCloseArchive: + RARCloseArchive(self._handle) + + def make_sure_ready(self): + if self.lockStatus == "locked": + raise InvalidRARArchiveUsage("cannot execute infoiter() without finishing previous one") + if self.lockStatus == "finished": + self.destruct() + self.init(self.password) + + def infoiter(self): + self.make_sure_ready() + return RarInfoIterator(self) + + def read_files(self, checker): + res = [] + for info in self.infoiter(): + if checker(info) and not info.isdir: + reader = PassiveReader() + c_callback = UNRARCALLBACK(reader._callback) + RARSetCallback(self._handle, c_callback, 1) + tmpres = RARProcessFile(self._handle, RAR_TEST, None, None) + if tmpres==ERAR_BAD_DATA: + raise IncorrectRARPassword + self.needskip = False + res.append((info, reader.get_result())) + return res + + + def extract(self, checker, path, withSubpath, overwrite): + res = [] + for info in self.infoiter(): + checkres = checker(info) + if checkres!=False and not info.isdir: + if checkres==True: + fn = info.filename + if not withSubpath: + fn = os.path.split(fn)[-1] + target = os.path.join(path, fn) + else: + raise DeprecationWarning, "Condition callbacks returning strings are deprecated and only supported in Windows" + target = checkres + if overwrite or (not os.path.exists(target)): + tmpres = RARProcessFile(self._handle, RAR_EXTRACT, None, target) + if tmpres==ERAR_BAD_DATA: + raise IncorrectRARPassword + + self.needskip = False + res.append(info) + return res + + diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..0d9bd7c --- /dev/null +++ b/__init__.py @@ -0,0 +1 @@ +__author__ = 'dromanin' diff --git a/comet.py b/comet.py new file mode 100644 index 0000000..1a06977 --- /dev/null +++ b/comet.py @@ -0,0 +1,260 @@ +""" +A python class to encapsulate CoMet data +""" + +""" +Copyright 2012-2014 Anthony Beville + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +from datetime import datetime +import zipfile +from pprint import pprint +import xml.etree.ElementTree as ET +from genericmetadata import GenericMetadata +import utils + +class CoMet: + + writer_synonyms = ['writer', 'plotter', 'scripter'] + penciller_synonyms = [ 'artist', 'penciller', 'penciler', 'breakdowns' ] + inker_synonyms = [ 'inker', 'artist', 'finishes' ] + colorist_synonyms = [ 'colorist', 'colourist', 'colorer', 'colourer' ] + letterer_synonyms = [ 'letterer'] + cover_synonyms = [ 'cover', 'covers', 'coverartist', 'cover artist' ] + editor_synonyms = [ 'editor'] + + def metadataFromString( self, string ): + + tree = ET.ElementTree(ET.fromstring( string )) + return self.convertXMLToMetadata( tree ) + + def stringFromMetadata( self, metadata ): + + header = '\n' + + tree = self.convertMetadataToXML( self, metadata ) + return header + ET.tostring(tree.getroot()) + + def indent( self, elem, level=0 ): + # for making the XML output readable + i = "\n" + level*" " + if len(elem): + if not elem.text or not elem.text.strip(): + elem.text = i + " " + if not elem.tail or not elem.tail.strip(): + elem.tail = i + for elem in elem: + self.indent( elem, level+1 ) + if not elem.tail or not elem.tail.strip(): + elem.tail = i + else: + if level and (not elem.tail or not elem.tail.strip()): + elem.tail = i + + def convertMetadataToXML( self, filename, metadata ): + + #shorthand for the metadata + md = metadata + + # build a tree structure + root = ET.Element("comet") + root.attrib['xmlns:comet'] = "http://www.denvog.com/comet/" + root.attrib['xmlns:xsi'] = "http://www.w3.org/2001/XMLSchema-instance" + root.attrib['xsi:schemaLocation'] = "http://www.denvog.com http://www.denvog.com/comet/comet.xsd" + + #helper func + def assign( comet_entry, md_entry): + if md_entry is not None: + ET.SubElement(root, comet_entry).text = u"{0}".format(md_entry) + + # title is manditory + if md.title is None: + md.title = "" + assign( 'title', md.title ) + assign( 'series', md.series ) + assign( 'issue', md.issue ) #must be int?? + assign( 'volume', md.volume ) + assign( 'description', md.comments ) + assign( 'publisher', md.publisher ) + assign( 'pages', md.pageCount ) + assign( 'format', md.format ) + assign( 'language', md.language ) + assign( 'rating', md.maturityRating ) + assign( 'price', md.price ) + assign( 'isVersionOf', md.isVersionOf ) + assign( 'rights', md.rights ) + assign( 'identifier', md.identifier ) + assign( 'lastMark', md.lastMark ) + assign( 'genre', md.genre ) # TODO repeatable + + if md.characters is not None: + char_list = [ c.strip() for c in md.characters.split(',') ] + for c in char_list: + assign( 'character', c ) + + if md.manga is not None and md.manga == "YesAndRightToLeft": + assign( 'readingDirection', "rtl") + + date_str = "" + if md.year is not None: + date_str = str(md.year).zfill(4) + if md.month is not None: + date_str += "-" + str(md.month).zfill(2) + assign( 'date', date_str ) + + assign( 'coverImage', md.coverImage ) + + # need to specially process the credits, since they are structured differently than CIX + credit_writer_list = list() + credit_penciller_list = list() + credit_inker_list = list() + credit_colorist_list = list() + credit_letterer_list = list() + credit_cover_list = list() + credit_editor_list = list() + + # loop thru credits, and build a list for each role that CoMet supports + for credit in metadata.credits: + + if credit['role'].lower() in set( self.writer_synonyms ): + ET.SubElement(root, 'writer').text = u"{0}".format(credit['person']) + + if credit['role'].lower() in set( self.penciller_synonyms ): + ET.SubElement(root, 'penciller').text = u"{0}".format(credit['person']) + + if credit['role'].lower() in set( self.inker_synonyms ): + ET.SubElement(root, 'inker').text = u"{0}".format(credit['person']) + + if credit['role'].lower() in set( self.colorist_synonyms ): + ET.SubElement(root, 'colorist').text = u"{0}".format(credit['person']) + + if credit['role'].lower() in set( self.letterer_synonyms ): + ET.SubElement(root, 'letterer').text = u"{0}".format(credit['person']) + + if credit['role'].lower() in set( self.cover_synonyms ): + ET.SubElement(root, 'coverDesigner').text = u"{0}".format(credit['person']) + + if credit['role'].lower() in set( self.editor_synonyms ): + ET.SubElement(root, 'editor').text = u"{0}".format(credit['person']) + + + # self pretty-print + self.indent(root) + + # wrap it in an ElementTree instance, and save as XML + tree = ET.ElementTree(root) + return tree + + + def convertXMLToMetadata( self, tree ): + + root = tree.getroot() + + if root.tag != 'comet': + raise 1 + return None + + metadata = GenericMetadata() + md = metadata + + # Helper function + def xlate( tag ): + node = root.find( tag ) + if node is not None: + return node.text + else: + return None + + md.series = xlate( 'series' ) + md.title = xlate( 'title' ) + md.issue = xlate( 'issue' ) + md.volume = xlate( 'volume' ) + md.comments = xlate( 'description' ) + md.publisher = xlate( 'publisher' ) + md.language = xlate( 'language' ) + md.format = xlate( 'format' ) + md.pageCount = xlate( 'pages' ) + md.maturityRating = xlate( 'rating' ) + md.price = xlate( 'price' ) + md.isVersionOf = xlate( 'isVersionOf' ) + md.rights = xlate( 'rights' ) + md.identifier = xlate( 'identifier' ) + md.lastMark = xlate( 'lastMark' ) + md.genre = xlate( 'genre' ) # TODO - repeatable field + + date = xlate( 'date' ) + if date is not None: + parts = date.split('-') + if len( parts) > 0: + md.year = parts[0] + if len( parts) > 1: + md.month = parts[1] + + md.coverImage = xlate( 'coverImage' ) + + readingDirection = xlate( 'readingDirection' ) + if readingDirection is not None and readingDirection == "rtl": + md.manga = "YesAndRightToLeft" + + # loop for character tags + char_list = [] + for n in root: + if n.tag == 'character': + char_list.append(n.text.strip()) + md.characters = utils.listToString( char_list ) + + # Now extract the credit info + for n in root: + if ( n.tag == 'writer' or + n.tag == 'penciller' or + n.tag == 'inker' or + n.tag == 'colorist' or + n.tag == 'letterer' or + n.tag == 'editor' + ): + metadata.addCredit( n.text.strip(), n.tag.title() ) + + if n.tag == 'coverDesigner': + metadata.addCredit( n.text.strip(), "Cover" ) + + + metadata.isEmpty = False + + return metadata + + #verify that the string actually contains CoMet data in XML format + def validateString( self, string ): + try: + tree = ET.ElementTree(ET.fromstring( string )) + root = tree.getroot() + if root.tag != 'comet': + raise Exception + except: + return False + + return True + + + def writeToExternalFile( self, filename, metadata ): + + tree = self.convertMetadataToXML( self, metadata ) + #ET.dump(tree) + tree.write(filename, encoding='utf-8') + + def readFromExternalFile( self, filename ): + + tree = ET.parse( filename ) + return self.convertXMLToMetadata( tree ) + diff --git a/comicarchive.py b/comicarchive.py new file mode 100644 index 0000000..381dc68 --- /dev/null +++ b/comicarchive.py @@ -0,0 +1,1088 @@ +""" +A python class to represent a single comic, be it file or folder of images +""" + +""" +Copyright 2012-2014 Anthony Beville + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +import zipfile +import os +import struct +import sys +import tempfile +import subprocess +import platform +import locale +from natsort import natsorted + +if platform.system() == "Windows": + import _subprocess +import time + +import StringIO +try: + import Image + pil_available = True +except ImportError: + pil_available = False + +sys.path.insert(0, os.path.abspath(".") ) +import UnRAR2 +from UnRAR2.rar_exceptions import * + +#from settings import ComicTaggerSettings +from comicinfoxml import ComicInfoXml +from comicbookinfo import ComicBookInfo +from comet import CoMet +from genericmetadata import GenericMetadata, PageType +from filenameparser import FileNameParser +from PyPDF2 import PdfFileReader + +class MetaDataStyle: + CBI = 0 + CIX = 1 + COMET = 2 + name = [ 'ComicBookLover', 'ComicRack', 'CoMet' ] + +class ZipArchiver: + + def __init__( self, path ): + self.path = path + + def getArchiveComment( self ): + zf = zipfile.ZipFile( self.path, 'r' ) + comment = zf.comment + zf.close() + return comment + + def setArchiveComment( self, comment ): + return self.writeZipComment( self.path, comment ) + + def readArchiveFile( self, archive_file ): + data = "" + zf = zipfile.ZipFile( self.path, 'r' ) + + try: + data = zf.read( archive_file ) + except zipfile.BadZipfile as e: + print >> sys.stderr, u"bad zipfile [{0}]: {1} :: {2}".format(e, self.path, archive_file) + zf.close() + raise IOError + except Exception as e: + zf.close() + print >> sys.stderr, u"bad zipfile [{0}]: {1} :: {2}".format(e, self.path, archive_file) + raise IOError + finally: + zf.close() + return data + + def removeArchiveFile( self, archive_file ): + try: + self.rebuildZipFile( [ archive_file ] ) + except: + return False + else: + return True + + def writeArchiveFile( self, archive_file, data ): + # At the moment, no other option but to rebuild the whole + # zip archive w/o the indicated file. Very sucky, but maybe + # another solution can be found + try: + self.rebuildZipFile( [ archive_file ] ) + + #now just add the archive file as a new one + zf = zipfile.ZipFile(self.path, mode='a', compression=zipfile.ZIP_DEFLATED ) + zf.writestr( archive_file, data ) + zf.close() + return True + except: + return False + + def getArchiveFilenameList( self ): + try: + zf = zipfile.ZipFile( self.path, 'r' ) + namelist = zf.namelist() + zf.close() + return namelist + except Exception as e: + print >> sys.stderr, u"Unable to get zipfile list [{0}]: {1}".format(e, self.path) + return [] + + # zip helper func + def rebuildZipFile( self, exclude_list ): + + # this recompresses the zip archive, without the files in the exclude_list + #print ">> sys.stderr, Rebuilding zip {0} without {1}".format( self.path, exclude_list ) + + # generate temp file + tmp_fd, tmp_name = tempfile.mkstemp( dir=os.path.dirname(self.path) ) + os.close( tmp_fd ) + + zin = zipfile.ZipFile (self.path, 'r') + zout = zipfile.ZipFile (tmp_name, 'w') + for item in zin.infolist(): + buffer = zin.read(item.filename) + if ( item.filename not in exclude_list ): + zout.writestr(item, buffer) + + #preserve the old comment + zout.comment = zin.comment + + zout.close() + zin.close() + + # replace with the new file + os.remove( self.path ) + os.rename( tmp_name, self.path ) + + + def writeZipComment( self, filename, comment ): + """ + This is a custom function for writing a comment to a zip file, + since the built-in one doesn't seem to work on Windows and Mac OS/X + + Fortunately, the zip comment is at the end of the file, and it's + easy to manipulate. See this website for more info: + see: http://en.wikipedia.org/wiki/Zip_(file_format)#Structure + """ + + #get file size + statinfo = os.stat(filename) + file_length = statinfo.st_size + + try: + fo = open(filename, "r+b") + + #the starting position, relative to EOF + pos = -4 + + found = False + value = bytearray() + + # walk backwards to find the "End of Central Directory" record + while ( not found ) and ( -pos != file_length ): + # seek, relative to EOF + fo.seek( pos, 2) + + value = fo.read( 4 ) + + #look for the end of central directory signature + if bytearray(value) == bytearray([ 0x50, 0x4b, 0x05, 0x06 ]): + found = True + else: + # not found, step back another byte + pos = pos - 1 + #print pos,"{1} int: {0:x}".format(bytearray(value)[0], value) + + if found: + + # now skip forward 20 bytes to the comment length word + pos += 20 + fo.seek( pos, 2) + + # Pack the length of the comment string + format = "H" # one 2-byte integer + comment_length = struct.pack(format, len(comment)) # pack integer in a binary string + + # write out the length + fo.write( comment_length ) + fo.seek( pos+2, 2) + + # write out the comment itself + fo.write( comment ) + fo.truncate() + fo.close() + else: + raise Exception('Failed to write comment to zip file!') + except: + return False + else: + return True + + def copyFromArchive( self, otherArchive ): + # Replace the current zip with one copied from another archive + try: + zout = zipfile.ZipFile (self.path, 'w') + for fname in otherArchive.getArchiveFilenameList(): + data = otherArchive.readArchiveFile( fname ) + if data is not None: + zout.writestr( fname, data ) + zout.close() + + #preserve the old comment + comment = otherArchive.getArchiveComment() + if comment is not None: + if not self.writeZipComment( self.path, comment ): + return False + except Exception as e: + print >> sys.stderr, u"Error while copying to {0}: {1}".format(self.path, e) + return False + else: + return True + + +#------------------------------------------ +# RAR implementation + +class RarArchiver: + + devnull = None + def __init__( self, path, rar_exe_path ): + self.path = path + self.rar_exe_path = rar_exe_path + + if RarArchiver.devnull is None: + RarArchiver.devnull = open(os.devnull, "w") + + # windows only, keeps the cmd.exe from popping up + if platform.system() == "Windows": + self.startupinfo = subprocess.STARTUPINFO() + self.startupinfo.dwFlags |= _subprocess.STARTF_USESHOWWINDOW + else: + self.startupinfo = None + + def __del__(self): + #RarArchiver.devnull.close() + pass + + def getArchiveComment( self ): + + rarc = self.getRARObj() + return rarc.comment + + def setArchiveComment( self, comment ): + + if self.rar_exe_path is not None: + try: + # write comment to temp file + tmp_fd, tmp_name = tempfile.mkstemp() + f = os.fdopen(tmp_fd, 'w+b') + f.write( comment ) + f.close() + + working_dir = os.path.dirname( os.path.abspath( self.path ) ) + + # use external program to write comment to Rar archive + subprocess.call([self.rar_exe_path, 'c', '-w' + working_dir , '-c-', '-z' + tmp_name, self.path], + startupinfo=self.startupinfo, + stdout=RarArchiver.devnull) + + if platform.system() == "Darwin": + time.sleep(1) + + os.remove( tmp_name) + except: + return False + else: + return True + else: + return False + + def readArchiveFile( self, archive_file ): + + # Make sure to escape brackets, since some funky stuff is going on + # underneath with "fnmatch" + archive_file = archive_file.replace("[", '[[]') + entries = [] + + rarc = self.getRARObj() + + tries = 0 + while tries < 7: + try: + tries = tries+1 + entries = rarc.read_files( archive_file ) + + if entries[0][0].size != len(entries[0][1]): + print >> sys.stderr, u"readArchiveFile(): [file is not expected size: {0} vs {1}] {2}:{3} [attempt # {4}]".format( + entries[0][0].size,len(entries[0][1]), self.path, archive_file, tries) + continue + + except (OSError, IOError) as e: + print >> sys.stderr, u"readArchiveFile(): [{0}] {1}:{2} attempt#{3}".format(str(e), self.path, archive_file, tries) + time.sleep(1) + except Exception as e: + print >> sys.stderr, u"Unexpected exception in readArchiveFile(): [{0}] for {1}:{2} attempt#{3}".format(str(e), self.path, archive_file, tries) + break + + else: + #Success" + #entries is a list of of tuples: ( rarinfo, filedata) + if tries > 1: + print >> sys.stderr, u"Attempted read_files() {0} times".format(tries) + if (len(entries) == 1): + return entries[0][1] + else: + raise IOError + + raise IOError + + + + def writeArchiveFile( self, archive_file, data ): + + if self.rar_exe_path is not None: + try: + tmp_folder = tempfile.mkdtemp() + + tmp_file = os.path.join( tmp_folder, archive_file ) + + working_dir = os.path.dirname( os.path.abspath( self.path ) ) + + # TODO: will this break if 'archive_file' is in a subfolder. i.e. "foo/bar.txt" + # will need to create the subfolder above, I guess... + f = open(tmp_file, 'w') + f.write( data ) + f.close() + + # use external program to write file to Rar archive + subprocess.call([self.rar_exe_path, 'a', '-w' + working_dir ,'-c-', '-ep', self.path, tmp_file], + startupinfo=self.startupinfo, + stdout=RarArchiver.devnull) + + if platform.system() == "Darwin": + time.sleep(1) + os.remove( tmp_file) + os.rmdir( tmp_folder) + except: + return False + else: + return True + else: + return False + + def removeArchiveFile( self, archive_file ): + if self.rar_exe_path is not None: + try: + # use external program to remove file from Rar archive + subprocess.call([self.rar_exe_path, 'd','-c-', self.path, archive_file], + startupinfo=self.startupinfo, + stdout=RarArchiver.devnull) + + if platform.system() == "Darwin": + time.sleep(1) + except: + return False + else: + return True + else: + return False + + def getArchiveFilenameList( self ): + + rarc = self.getRARObj() + #namelist = [ item.filename for item in rarc.infolist() ] + #return namelist + + tries = 0 + while tries < 7: + try: + tries = tries+1 + #namelist = [ item.filename for item in rarc.infolist() ] + namelist = [] + for item in rarc.infolist(): + if item.size != 0: + namelist.append( item.filename ) + + except (OSError, IOError) as e: + print >> sys.stderr, u"getArchiveFilenameList(): [{0}] {1} attempt#{2}".format(str(e), self.path, tries) + time.sleep(1) + + else: + #Success" + return namelist + + raise e + + + def getRARObj( self ): + tries = 0 + while tries < 7: + try: + tries = tries+1 + rarc = UnRAR2.RarFile( self.path ) + + except (OSError, IOError) as e: + print >> sys.stderr, u"getRARObj(): [{0}] {1} attempt#{2}".format(str(e), self.path, tries) + time.sleep(1) + + else: + #Success" + return rarc + + raise e + +#------------------------------------------ +# Folder implementation +class FolderArchiver: + + def __init__( self, path ): + self.path = path + self.comment_file_name = "ComicTaggerFolderComment.txt" + + def getArchiveComment( self ): + return self.readArchiveFile( self.comment_file_name ) + + def setArchiveComment( self, comment ): + return self.writeArchiveFile( self.comment_file_name, comment ) + + def readArchiveFile( self, archive_file ): + + data = "" + fname = os.path.join( self.path, archive_file ) + try: + with open( fname, 'rb' ) as f: + data = f.read() + f.close() + except IOError as e: + pass + + return data + + def writeArchiveFile( self, archive_file, data ): + + fname = os.path.join( self.path, archive_file ) + try: + with open(fname, 'w+') as f: + f.write( data ) + f.close() + except: + return False + else: + return True + + def removeArchiveFile( self, archive_file ): + + fname = os.path.join( self.path, archive_file ) + try: + os.remove( fname ) + except: + return False + else: + return True + + def getArchiveFilenameList( self ): + return self.listFiles( self.path ) + + def listFiles( self, folder ): + + itemlist = list() + + for item in os.listdir( folder ): + itemlist.append( item ) + if os.path.isdir( item ): + itemlist.extend( self.listFiles( os.path.join( folder, item ) )) + + return itemlist + +#------------------------------------------ +# Unknown implementation +class UnknownArchiver: + + def __init__( self, path ): + self.path = path + + def getArchiveComment( self ): + return "" + def setArchiveComment( self, comment ): + return False + def readArchiveFile( self ): + return "" + def writeArchiveFile( self, archive_file, data ): + return False + def removeArchiveFile( self, archive_file ): + return False + def getArchiveFilenameList( self ): + return [] + +class PdfArchiver: + def __init__( self, path ): + self.path = path + + def getArchiveComment( self ): + return "" + def setArchiveComment( self, comment ): + return False + def readArchiveFile( self, page_num ): + return subprocess.check_output(['mudraw', '-o','-', self.path, str(int(os.path.basename(page_num)[:-4]))]) + def writeArchiveFile( self, archive_file, data ): + return False + def removeArchiveFile( self, archive_file ): + return False + def getArchiveFilenameList( self ): + out = [] + pdf = PdfFileReader(open(self.path, 'rb')) + for page in range(1, pdf.getNumPages() + 1): + out.append("/%04d.jpg" % (page)) + return out + +#------------------------------------------------------------------ +class ComicArchive: + + logo_data = None + + class ArchiveType: + Zip, Rar, Folder, Pdf, Unknown = range(5) + + def __init__( self, path, rar_exe_path=None, default_image_path=None ): + self.path = path + + self.rar_exe_path = rar_exe_path + self.ci_xml_filename = 'ComicInfo.xml' + self.comet_default_filename = 'CoMet.xml' + self.resetCache() + self.default_image_path = default_image_path + + # Use file extension to decide which archive test we do first + ext = os.path.splitext(path)[1].lower() + + self.archive_type = self.ArchiveType.Unknown + self.archiver = UnknownArchiver( self.path ) + + if ext == ".cbr" or ext == ".rar": + if self.rarTest(): + self.archive_type = self.ArchiveType.Rar + self.archiver = RarArchiver( self.path, rar_exe_path=self.rar_exe_path ) + + elif self.zipTest(): + self.archive_type = self.ArchiveType.Zip + self.archiver = ZipArchiver( self.path ) + else: + if self.zipTest(): + self.archive_type = self.ArchiveType.Zip + self.archiver = ZipArchiver( self.path ) + + elif self.rarTest(): + self.archive_type = self.ArchiveType.Rar + self.archiver = RarArchiver( self.path, rar_exe_path=self.rar_exe_path ) + elif os.path.basename(self.path)[-3:] == 'pdf': + self.archive_type = self.ArchiveType.Pdf + self.archiver = PdfArchiver(self.path) + + if ComicArchive.logo_data is None: + #fname = ComicTaggerSettings.getGraphic('nocover.png') + fname = self.default_image_path + with open(fname, 'rb') as fd: + ComicArchive.logo_data = fd.read() + + # Clears the cached data + def resetCache( self ): + self.has_cix = None + self.has_cbi = None + self.has_comet = None + self.comet_filename = None + self.page_count = None + self.page_list = None + self.cix_md = None + self.cbi_md = None + self.comet_md = None + + def loadCache( self, style_list ): + for style in style_list: + self.readMetadata(style) + + def rename( self, path ): + self.path = path + self.archiver.path = path + + def zipTest( self ): + return zipfile.is_zipfile( self.path ) + + def rarTest( self ): + try: + rarc = UnRAR2.RarFile( self.path ) + except: # InvalidRARArchive: + return False + else: + return True + + + def isZip( self ): + return self.archive_type == self.ArchiveType.Zip + + def isRar( self ): + return self.archive_type == self.ArchiveType.Rar + def isPdf(self): + return self.archive_type == self.ArchiveType.Pdf + def isFolder( self ): + return self.archive_type == self.ArchiveType.Folder + + def isWritable( self, check_rar_status=True ): + if self.archive_type == self.ArchiveType.Unknown : + return False + + elif check_rar_status and self.isRar() and self.rar_exe_path is None: + return False + + elif not os.access(self.path, os.W_OK): + return False + + elif ((self.archive_type != self.ArchiveType.Folder) and + (not os.access( os.path.dirname( os.path.abspath(self.path)), os.W_OK ))): + return False + + return True + + def isWritableForStyle( self, data_style ): + + if self.isRar() and data_style == MetaDataStyle.CBI: + return False + + return self.isWritable() + + def seemsToBeAComicArchive( self ): + + # Do we even care about extensions?? + ext = os.path.splitext(self.path)[1].lower() + + if ( + ( self.isZip() or self.isRar() or self.isPdf()) #or self.isFolder() ) + and + ( self.getNumberOfPages() > 0) + + ): + return True + else: + return False + + def readMetadata( self, style ): + + if style == MetaDataStyle.CIX: + return self.readCIX() + elif style == MetaDataStyle.CBI: + return self.readCBI() + elif style == MetaDataStyle.COMET: + return self.readCoMet() + else: + return GenericMetadata() + + def writeMetadata( self, metadata, style ): + + retcode = None + if style == MetaDataStyle.CIX: + retcode = self.writeCIX( metadata ) + elif style == MetaDataStyle.CBI: + retcode = self.writeCBI( metadata ) + elif style == MetaDataStyle.COMET: + retcode = self.writeCoMet( metadata ) + return retcode + + + def hasMetadata( self, style ): + + if style == MetaDataStyle.CIX: + return self.hasCIX() + elif style == MetaDataStyle.CBI: + return self.hasCBI() + elif style == MetaDataStyle.COMET: + return self.hasCoMet() + else: + return False + + def removeMetadata( self, style ): + retcode = True + if style == MetaDataStyle.CIX: + retcode = self.removeCIX() + elif style == MetaDataStyle.CBI: + retcode = self.removeCBI() + elif style == MetaDataStyle.COMET: + retcode = self.removeCoMet() + return retcode + + def getPage( self, index ): + + image_data = None + + filename = self.getPageName( index ) + + if filename is not None: + try: + image_data = self.archiver.readArchiveFile( filename ) + except IOError: + print >> sys.stderr, u"Error reading in page. Substituting logo page." + image_data = ComicArchive.logo_data + + return image_data + + def getPageName( self, index ): + + if index is None: + return None + + page_list = self.getPageNameList() + + num_pages = len( page_list ) + if num_pages == 0 or index >= num_pages: + return None + + return page_list[index] + + def getScannerPageIndex( self ): + + scanner_page_index = None + + #make a guess at the scanner page + name_list = self.getPageNameList() + count = self.getNumberOfPages() + + #too few pages to really know + if count < 5: + return None + + # count the length of every filename, and count occurences + length_buckets = dict() + for name in name_list: + fname = os.path.split(name)[1] + length = len(fname) + if length_buckets.has_key( length ): + length_buckets[ length ] += 1 + else: + length_buckets[ length ] = 1 + + # sort by most common + sorted_buckets = sorted(length_buckets.iteritems(), key=lambda (k,v): (v,k), reverse=True) + + # statistical mode occurence is first + mode_length = sorted_buckets[0][0] + + # we are only going to consider the final image file: + final_name = os.path.split(name_list[count-1])[1] + + common_length_list = list() + for name in name_list: + if len(os.path.split(name)[1]) == mode_length: + common_length_list.append( os.path.split(name)[1] ) + + prefix = os.path.commonprefix(common_length_list) + + if mode_length <= 7 and prefix == "": + #probably all numbers + if len(final_name) > mode_length: + scanner_page_index = count-1 + + # see if the last page doesn't start with the same prefix as most others + elif not final_name.startswith(prefix): + scanner_page_index = count-1 + + return scanner_page_index + + + def getPageNameList( self , sort_list=True): + + if self.page_list is None: + # get the list file names in the archive, and sort + files = self.archiver.getArchiveFilenameList() + + # seems like some archive creators are on Windows, and don't know about case-sensitivity! + if sort_list: + def keyfunc(k): + #hack to account for some weird scanner ID pages + #basename=os.path.split(k)[1] + #if basename < '0': + # k = os.path.join(os.path.split(k)[0], "z" + basename) + return k.lower() + + files = natsorted(files, key=keyfunc,signed=False) + + # make a sub-list of image files + self.page_list = [] + for name in files: + if ( name[-4:].lower() in [ ".jpg", "jpeg", ".png", ".gif", "webp" ] and os.path.basename(name)[0] != "." ): + self.page_list.append(name) + + return self.page_list + + def getNumberOfPages( self ): + + if self.page_count is None: + self.page_count = len( self.getPageNameList( ) ) + return self.page_count + + def readCBI( self ): + if self.cbi_md is None: + raw_cbi = self.readRawCBI() + if raw_cbi is None: + self.cbi_md = GenericMetadata() + else: + self.cbi_md = ComicBookInfo().metadataFromString( raw_cbi ) + + self.cbi_md.setDefaultPageList( self.getNumberOfPages() ) + + return self.cbi_md + + def readRawCBI( self ): + if ( not self.hasCBI() ): + return None + + return self.archiver.getArchiveComment() + + def hasCBI(self): + if self.has_cbi is None: + + #if ( not ( self.isZip() or self.isRar()) or not self.seemsToBeAComicArchive() ): + if not self.seemsToBeAComicArchive(): + self.has_cbi = False + else: + comment = self.archiver.getArchiveComment() + self.has_cbi = ComicBookInfo().validateString( comment ) + + return self.has_cbi + + def writeCBI( self, metadata ): + if metadata is not None: + self.applyArchiveInfoToMetadata( metadata ) + cbi_string = ComicBookInfo().stringFromMetadata( metadata ) + write_success = self.archiver.setArchiveComment( cbi_string ) + if write_success: + self.has_cbi = True + self.cbi_md = metadata + self.resetCache() + return write_success + else: + return False + + def removeCBI( self ): + if self.hasCBI(): + write_success = self.archiver.setArchiveComment( "" ) + if write_success: + self.has_cbi = False + self.cbi_md = None + self.resetCache() + return write_success + return True + + def readCIX( self ): + if self.cix_md is None: + raw_cix = self.readRawCIX() + if raw_cix is None or raw_cix == "": + self.cix_md = GenericMetadata() + else: + self.cix_md = ComicInfoXml().metadataFromString( raw_cix ) + + #validate the existing page list (make sure count is correct) + if len ( self.cix_md.pages ) != 0 : + if len ( self.cix_md.pages ) != self.getNumberOfPages(): + # pages array doesn't match the actual number of images we're seeing + # in the archive, so discard the data + self.cix_md.pages = [] + + if len( self.cix_md.pages ) == 0: + self.cix_md.setDefaultPageList( self.getNumberOfPages() ) + + return self.cix_md + + def readRawCIX( self ): + if not self.hasCIX(): + return None + try: + raw_cix = self.archiver.readArchiveFile( self.ci_xml_filename ) + except IOError: + print "Error reading in raw CIX!" + raw_cix = "" + return raw_cix + + def writeCIX(self, metadata): + + if metadata is not None: + self.applyArchiveInfoToMetadata( metadata, calc_page_sizes=True ) + cix_string = ComicInfoXml().stringFromMetadata( metadata ) + write_success = self.archiver.writeArchiveFile( self.ci_xml_filename, cix_string ) + if write_success: + self.has_cix = True + self.cix_md = metadata + self.resetCache() + return write_success + else: + return False + + def removeCIX( self ): + if self.hasCIX(): + write_success = self.archiver.removeArchiveFile( self.ci_xml_filename ) + if write_success: + self.has_cix = False + self.cix_md = None + self.resetCache() + return write_success + return True + + + def hasCIX(self): + if self.has_cix is None: + + if not self.seemsToBeAComicArchive(): + self.has_cix = False + elif self.ci_xml_filename in self.archiver.getArchiveFilenameList(): + self.has_cix = True + else: + self.has_cix = False + return self.has_cix + + + def readCoMet( self ): + if self.comet_md is None: + raw_comet = self.readRawCoMet() + if raw_comet is None or raw_comet == "": + self.comet_md = GenericMetadata() + else: + self.comet_md = CoMet().metadataFromString( raw_comet ) + + self.comet_md.setDefaultPageList( self.getNumberOfPages() ) + #use the coverImage value from the comet_data to mark the cover in this struct + # walk through list of images in file, and find the matching one for md.coverImage + # need to remove the existing one in the default + if self.comet_md.coverImage is not None: + cover_idx = 0 + for idx,f in enumerate(self.getPageNameList()): + if self.comet_md.coverImage == f: + cover_idx = idx + break + if cover_idx != 0: + del (self.comet_md.pages[0]['Type'] ) + self.comet_md.pages[ cover_idx ]['Type'] = PageType.FrontCover + + return self.comet_md + + def readRawCoMet( self ): + if not self.hasCoMet(): + print >> sys.stderr, self.path, "doesn't have CoMet data!" + return None + + try: + raw_comet = self.archiver.readArchiveFile( self.comet_filename ) + except IOError: + print >> sys.stderr, u"Error reading in raw CoMet!" + raw_comet = "" + return raw_comet + + def writeCoMet(self, metadata): + + if metadata is not None: + if not self.hasCoMet(): + self.comet_filename = self.comet_default_filename + + self.applyArchiveInfoToMetadata( metadata ) + # Set the coverImage value, if it's not the first page + cover_idx = int(metadata.getCoverPageIndexList()[0]) + if cover_idx != 0: + metadata.coverImage = self.getPageName( cover_idx ) + + comet_string = CoMet().stringFromMetadata( metadata ) + write_success = self.archiver.writeArchiveFile( self.comet_filename, comet_string ) + if write_success: + self.has_comet = True + self.comet_md = metadata + self.resetCache() + return write_success + else: + return False + + def removeCoMet( self ): + if self.hasCoMet(): + write_success = self.archiver.removeArchiveFile( self.comet_filename ) + if write_success: + self.has_comet = False + self.comet_md = None + self.resetCache() + return write_success + return True + + def hasCoMet(self): + if self.has_comet is None: + self.has_comet = False + if not self.seemsToBeAComicArchive(): + return self.has_comet + + #look at all xml files in root, and search for CoMet data, get first + for n in self.archiver.getArchiveFilenameList(): + if ( os.path.dirname(n) == "" and + os.path.splitext(n)[1].lower() == '.xml'): + # read in XML file, and validate it + try: + data = self.archiver.readArchiveFile( n ) + except: + data = "" + print >> sys.stderr, u"Error reading in Comet XML for validation!" + if CoMet().validateString( data ): + # since we found it, save it! + self.comet_filename = n + self.has_comet = True + break + + return self.has_comet + + + + def applyArchiveInfoToMetadata( self, md, calc_page_sizes=False): + md.pageCount = self.getNumberOfPages() + + if calc_page_sizes: + for p in md.pages: + idx = int( p['Image'] ) + if pil_available: + if 'ImageSize' not in p or 'ImageHeight' not in p or 'ImageWidth' not in p: + data = self.getPage( idx ) + if data is not None: + try: + im = Image.open(StringIO.StringIO(data)) + w,h = im.size + + p['ImageSize'] = str(len(data)) + p['ImageHeight'] = str(h) + p['ImageWidth'] = str(w) + except IOError: + p['ImageSize'] = str(len(data)) + + else: + if 'ImageSize' not in p: + data = self.getPage( idx ) + p['ImageSize'] = str(len(data)) + + + + def metadataFromFilename( self , parse_scan_info=True): + + metadata = GenericMetadata() + + fnp = FileNameParser() + fnp.parseFilename( self.path ) + + if fnp.issue != "": + metadata.issue = fnp.issue + if fnp.series != "": + metadata.series = fnp.series + if fnp.volume != "": + metadata.volume = fnp.volume + if fnp.year != "": + metadata.year = fnp.year + if fnp.issue_count != "": + metadata.issueCount = fnp.issue_count + if parse_scan_info: + if fnp.remainder != "": + metadata.scanInfo = fnp.remainder + + metadata.isEmpty = False + + return metadata + + def exportAsZip( self, zipfilename ): + if self.archive_type == self.ArchiveType.Zip: + # nothing to do, we're already a zip + return True + + zip_archiver = ZipArchiver( zipfilename ) + return zip_archiver.copyFromArchive( self.archiver ) + diff --git a/comicbookinfo.py b/comicbookinfo.py new file mode 100644 index 0000000..a0bbaf0 --- /dev/null +++ b/comicbookinfo.py @@ -0,0 +1,152 @@ +""" +A python class to encapsulate the ComicBookInfo data +""" + +""" +Copyright 2012-2014 Anthony Beville + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + + +import json +from datetime import datetime +import zipfile + +from genericmetadata import GenericMetadata +import utils +#import ctversion + +class ComicBookInfo: + + + def metadataFromString( self, string ): + + cbi_container = json.loads( unicode(string, 'utf-8') ) + + metadata = GenericMetadata() + + cbi = cbi_container[ 'ComicBookInfo/1.0' ] + + #helper func + # If item is not in CBI, return None + def xlate( cbi_entry): + if cbi_entry in cbi: + return cbi[cbi_entry] + else: + return None + + metadata.series = xlate( 'series' ) + metadata.title = xlate( 'title' ) + metadata.issue = xlate( 'issue' ) + metadata.publisher = xlate( 'publisher' ) + metadata.month = xlate( 'publicationMonth' ) + metadata.year = xlate( 'publicationYear' ) + metadata.issueCount = xlate( 'numberOfIssues' ) + metadata.comments = xlate( 'comments' ) + metadata.credits = xlate( 'credits' ) + metadata.genre = xlate( 'genre' ) + metadata.volume = xlate( 'volume' ) + metadata.volumeCount = xlate( 'numberOfVolumes' ) + metadata.language = xlate( 'language' ) + metadata.country = xlate( 'country' ) + metadata.criticalRating = xlate( 'rating' ) + metadata.tags = xlate( 'tags' ) + + # make sure credits and tags are at least empty lists and not None + if metadata.credits is None: + metadata.credits = [] + if metadata.tags is None: + metadata.tags = [] + + #need to massage the language string to be ISO + if metadata.language is not None: + # reverse look-up + pattern = metadata.language + metadata.language = None + for key in utils.getLanguageDict(): + if utils.getLanguageDict()[ key ] == pattern.encode('utf-8'): + metadata.language = key + break + + metadata.isEmpty = False + + return metadata + + def stringFromMetadata( self, metadata ): + + cbi_container = self.createJSONDictionary( metadata ) + return json.dumps( cbi_container ) + + #verify that the string actually contains CBI data in JSON format + def validateString( self, string ): + + try: + cbi_container = json.loads( string ) + except: + return False + + return ( 'ComicBookInfo/1.0' in cbi_container ) + + + def createJSONDictionary( self, metadata ): + + # Create the dictionary that we will convert to JSON text + cbi = dict() + cbi_container = {'appID' : 'ComicTagger/' + '1.0.0', #ctversion.version, + 'lastModified' : str(datetime.now()), + 'ComicBookInfo/1.0' : cbi } + + #helper func + def assign( cbi_entry, md_entry): + if md_entry is not None: + cbi[cbi_entry] = md_entry + + #helper func + def toInt(s): + i = None + if type(s) in [ str, unicode, int ]: + try: + i = int(s) + except ValueError: + pass + return i + + assign( 'series', metadata.series ) + assign( 'title', metadata.title ) + assign( 'issue', metadata.issue ) + assign( 'publisher', metadata.publisher ) + assign( 'publicationMonth', toInt(metadata.month) ) + assign( 'publicationYear', toInt(metadata.year) ) + assign( 'numberOfIssues', toInt(metadata.issueCount) ) + assign( 'comments', metadata.comments ) + assign( 'genre', metadata.genre ) + assign( 'volume', toInt(metadata.volume) ) + assign( 'numberOfVolumes', toInt(metadata.volumeCount) ) + assign( 'language', utils.getLanguageFromISO(metadata.language) ) + assign( 'country', metadata.country ) + assign( 'rating', metadata.criticalRating ) + assign( 'credits', metadata.credits ) + assign( 'tags', metadata.tags ) + + return cbi_container + + + def writeToExternalFile( self, filename, metadata ): + + cbi_container = self.createJSONDictionary(metadata) + + f = open(filename, 'w') + f.write(json.dumps(cbi_container, indent=4)) + f.close + diff --git a/comicinfoxml.py b/comicinfoxml.py new file mode 100644 index 0000000..9e9df07 --- /dev/null +++ b/comicinfoxml.py @@ -0,0 +1,293 @@ +""" +A python class to encapsulate ComicRack's ComicInfo.xml data +""" + +""" +Copyright 2012-2014 Anthony Beville + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +from datetime import datetime +import zipfile +from pprint import pprint +import xml.etree.ElementTree as ET +from genericmetadata import GenericMetadata +import utils + +class ComicInfoXml: + + writer_synonyms = ['writer', 'plotter', 'scripter'] + penciller_synonyms = [ 'artist', 'penciller', 'penciler', 'breakdowns' ] + inker_synonyms = [ 'inker', 'artist', 'finishes' ] + colorist_synonyms = [ 'colorist', 'colourist', 'colorer', 'colourer' ] + letterer_synonyms = [ 'letterer'] + cover_synonyms = [ 'cover', 'covers', 'coverartist', 'cover artist' ] + editor_synonyms = [ 'editor'] + + + def getParseableCredits( self ): + parsable_credits = [] + parsable_credits.extend( self.writer_synonyms ) + parsable_credits.extend( self.penciller_synonyms ) + parsable_credits.extend( self.inker_synonyms ) + parsable_credits.extend( self.colorist_synonyms ) + parsable_credits.extend( self.letterer_synonyms ) + parsable_credits.extend( self.cover_synonyms ) + parsable_credits.extend( self.editor_synonyms ) + return parsable_credits + + def metadataFromString( self, string ): + + tree = ET.ElementTree(ET.fromstring( string )) + return self.convertXMLToMetadata( tree ) + + def stringFromMetadata( self, metadata ): + + header = '\n' + + tree = self.convertMetadataToXML( self, metadata ) + return header + ET.tostring(tree.getroot()) + + def indent( self, elem, level=0 ): + # for making the XML output readable + i = "\n" + level*" " + if len(elem): + if not elem.text or not elem.text.strip(): + elem.text = i + " " + if not elem.tail or not elem.tail.strip(): + elem.tail = i + for elem in elem: + self.indent( elem, level+1 ) + if not elem.tail or not elem.tail.strip(): + elem.tail = i + else: + if level and (not elem.tail or not elem.tail.strip()): + elem.tail = i + + def convertMetadataToXML( self, filename, metadata ): + + #shorthand for the metadata + md = metadata + + # build a tree structure + root = ET.Element("ComicInfo") + root.attrib['xmlns:xsi']="http://www.w3.org/2001/XMLSchema-instance" + root.attrib['xmlns:xsd']="http://www.w3.org/2001/XMLSchema" + #helper func + def assign( cix_entry, md_entry): + if md_entry is not None: + ET.SubElement(root, cix_entry).text = u"{0}".format(md_entry) + + assign( 'Title', md.title ) + assign( 'Series', md.series ) + assign( 'Number', md.issue ) + assign( 'Count', md.issueCount ) + assign( 'Volume', md.volume ) + assign( 'AlternateSeries', md.alternateSeries ) + assign( 'AlternateNumber', md.alternateNumber ) + assign( 'StoryArc', md.storyArc ) + assign( 'SeriesGroup', md.seriesGroup ) + assign( 'AlternateCount', md.alternateCount ) + assign( 'Summary', md.comments ) + assign( 'Notes', md.notes ) + assign( 'Year', md.year ) + assign( 'Month', md.month ) + assign( 'Day', md.day ) + + # need to specially process the credits, since they are structured differently than CIX + credit_writer_list = list() + credit_penciller_list = list() + credit_inker_list = list() + credit_colorist_list = list() + credit_letterer_list = list() + credit_cover_list = list() + credit_editor_list = list() + + # first, loop thru credits, and build a list for each role that CIX supports + for credit in metadata.credits: + + if credit['role'].lower() in set( self.writer_synonyms ): + credit_writer_list.append(credit['person'].replace(",","")) + + if credit['role'].lower() in set( self.penciller_synonyms ): + credit_penciller_list.append(credit['person'].replace(",","")) + + if credit['role'].lower() in set( self.inker_synonyms ): + credit_inker_list.append(credit['person'].replace(",","")) + + if credit['role'].lower() in set( self.colorist_synonyms ): + credit_colorist_list.append(credit['person'].replace(",","")) + + if credit['role'].lower() in set( self.letterer_synonyms ): + credit_letterer_list.append(credit['person'].replace(",","")) + + if credit['role'].lower() in set( self.cover_synonyms ): + credit_cover_list.append(credit['person'].replace(",","")) + + if credit['role'].lower() in set( self.editor_synonyms ): + credit_editor_list.append(credit['person'].replace(",","")) + + # second, convert each list to string, and add to XML struct + if len( credit_writer_list ) > 0: + node = ET.SubElement(root, 'Writer') + node.text = utils.listToString( credit_writer_list ) + + if len( credit_penciller_list ) > 0: + node = ET.SubElement(root, 'Penciller') + node.text = utils.listToString( credit_penciller_list ) + + if len( credit_inker_list ) > 0: + node = ET.SubElement(root, 'Inker') + node.text = utils.listToString( credit_inker_list ) + + if len( credit_colorist_list ) > 0: + node = ET.SubElement(root, 'Colorist') + node.text = utils.listToString( credit_colorist_list ) + + if len( credit_letterer_list ) > 0: + node = ET.SubElement(root, 'Letterer') + node.text = utils.listToString( credit_letterer_list ) + + if len( credit_cover_list ) > 0: + node = ET.SubElement(root, 'CoverArtist') + node.text = utils.listToString( credit_cover_list ) + + if len( credit_editor_list ) > 0: + node = ET.SubElement(root, 'Editor') + node.text = utils.listToString( credit_editor_list ) + + assign( 'Publisher', md.publisher ) + assign( 'Imprint', md.imprint ) + assign( 'Genre', md.genre ) + assign( 'Web', md.webLink ) + assign( 'PageCount', md.pageCount ) + assign( 'LanguageISO', md.language ) + assign( 'Format', md.format ) + assign( 'AgeRating', md.maturityRating ) + if md.blackAndWhite is not None and md.blackAndWhite: + ET.SubElement(root, 'BlackAndWhite').text = "Yes" + assign( 'Manga', md.manga ) + assign( 'Characters', md.characters ) + assign( 'Teams', md.teams ) + assign( 'Locations', md.locations ) + assign( 'ScanInformation', md.scanInfo ) + + # loop and add the page entries under pages node + if len( md.pages ) > 0: + pages_node = ET.SubElement(root, 'Pages') + for page_dict in md.pages: + page_node = ET.SubElement(pages_node, 'Page') + page_node.attrib = page_dict + + # self pretty-print + self.indent(root) + + # wrap it in an ElementTree instance, and save as XML + tree = ET.ElementTree(root) + return tree + + + def convertXMLToMetadata( self, tree ): + + root = tree.getroot() + + if root.tag != 'ComicInfo': + raise 1 + return None + + metadata = GenericMetadata() + md = metadata + + + # Helper function + def xlate( tag ): + node = root.find( tag ) + if node is not None: + return node.text + else: + return None + + md.series = xlate( 'Series' ) + md.title = xlate( 'Title' ) + md.issue = xlate( 'Number' ) + md.issueCount = xlate( 'Count' ) + md.volume = xlate( 'Volume' ) + md.alternateSeries = xlate( 'AlternateSeries' ) + md.alternateNumber = xlate( 'AlternateNumber' ) + md.alternateCount = xlate( 'AlternateCount' ) + md.comments = xlate( 'Summary' ) + md.notes = xlate( 'Notes' ) + md.year = xlate( 'Year' ) + md.month = xlate( 'Month' ) + md.day = xlate( 'Day' ) + md.publisher = xlate( 'Publisher' ) + md.imprint = xlate( 'Imprint' ) + md.genre = xlate( 'Genre' ) + md.webLink = xlate( 'Web' ) + md.language = xlate( 'LanguageISO' ) + md.format = xlate( 'Format' ) + md.manga = xlate( 'Manga' ) + md.characters = xlate( 'Characters' ) + md.teams = xlate( 'Teams' ) + md.locations = xlate( 'Locations' ) + md.pageCount = xlate( 'PageCount' ) + md.scanInfo = xlate( 'ScanInformation' ) + md.storyArc = xlate( 'StoryArc' ) + md.seriesGroup = xlate( 'SeriesGroup' ) + md.maturityRating = xlate( 'AgeRating' ) + + tmp = xlate( 'BlackAndWhite' ) + md.blackAndWhite = False + if tmp is not None and tmp.lower() in [ "yes", "true", "1" ]: + md.blackAndWhite = True + # Now extract the credit info + for n in root: + if ( n.tag == 'Writer' or + n.tag == 'Penciller' or + n.tag == 'Inker' or + n.tag == 'Colorist' or + n.tag == 'Letterer' or + n.tag == 'Editor' + ): + if n.text is not None: + for name in n.text.split(','): + metadata.addCredit( name.strip(), n.tag ) + + if n.tag == 'CoverArtist': + if n.text is not None: + for name in n.text.split(','): + metadata.addCredit( name.strip(), "Cover" ) + + # parse page data now + pages_node = root.find( "Pages" ) + if pages_node is not None: + for page in pages_node: + metadata.pages.append( page.attrib ) + #print page.attrib + + metadata.isEmpty = False + + return metadata + + def writeToExternalFile( self, filename, metadata ): + + tree = self.convertMetadataToXML( self, metadata ) + #ET.dump(tree) + tree.write(filename, encoding='utf-8') + + def readFromExternalFile( self, filename ): + + tree = ET.parse( filename ) + return self.convertXMLToMetadata( tree ) + diff --git a/filenameparser.py b/filenameparser.py new file mode 100644 index 0000000..6f3aa05 --- /dev/null +++ b/filenameparser.py @@ -0,0 +1,277 @@ +""" +Functions for parsing comic info from filename + +This should probably be re-written, but, well, it mostly works! + +""" + +""" +Copyright 2012-2014 Anthony Beville + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + + +# Some portions of this code were modified from pyComicMetaThis project +# http://code.google.com/p/pycomicmetathis/ + +import re +import os +from urllib import unquote + +class FileNameParser: + + def repl(self, m): + return ' ' * len(m.group()) + + def fixSpaces( self, string, remove_dashes=True ): + if remove_dashes: + placeholders = ['[-_]',' +'] + else: + placeholders = ['[_]',' +'] + for ph in placeholders: + string = re.sub(ph, self.repl, string ) + return string #.strip() + + + def getIssueCount( self,filename, issue_end ): + + count = "" + filename = filename[issue_end:] + + # replace any name seperators with spaces + tmpstr = self.fixSpaces(filename) + found = False + + match = re.search('(?<=\sof\s)\d+(?=\s)', tmpstr, re.IGNORECASE) + if match: + count = match.group() + found = True + + if not found: + match = re.search('(?<=\(of\s)\d+(?=\))', tmpstr, re.IGNORECASE) + if match: + count = match.group() + found = True + + + count = count.lstrip("0") + + return count + + def getIssueNumber( self, filename ): + + # Returns a tuple of issue number string, and start and end indexs in the filename + # (The indexes will be used to split the string up for further parsing) + + found = False + issue = '' + start = 0 + end = 0 + + # first, look for multiple "--", this means it's formatted differently from most: + if "--" in filename: + # the pattern seems to be that anything to left of the first "--" is the series name followed by issue + filename = re.sub("--.*", self.repl, filename) + + elif "__" in filename: + # the pattern seems to be that anything to left of the first "__" is the series name followed by issue + filename = re.sub("__.*", self.repl, filename) + + filename = filename.replace("+", " ") + + # replace parenthetical phrases with spaces + filename = re.sub( "\(.*?\)", self.repl, filename) + filename = re.sub( "\[.*?\]", self.repl, filename) + + # replace any name seperators with spaces + filename = self.fixSpaces(filename) + + # remove any "of NN" phrase with spaces (problem: this could break on some titles) + filename = re.sub( "of [\d]+", self.repl, filename) + + #print u"[{0}]".format(filename) + + # we should now have a cleaned up filename version with all the words in + # the same positions as original filename + + # make a list of each word and its position + word_list = list() + for m in re.finditer("\S+", filename): + word_list.append( (m.group(0), m.start(), m.end()) ) + + # remove the first word, since it can't be the issue number + if len(word_list) > 1: + word_list = word_list[1:] + else: + #only one word?? just bail. + return issue, start, end + + # Now try to search for the likely issue number word in the list + + # first look for a word with "#" followed by digits with optional sufix + # this is almost certainly the issue number + for w in reversed(word_list): + if re.match("#[-]?(([0-9]*\.[0-9]+|[0-9]+)(\w*))", w[0]): + found = True + break + + # same as above but w/o a '#', and only look at the last word in the list + if not found: + w = word_list[-1] + if re.match("[-]?(([0-9]*\.[0-9]+|[0-9]+)(\w*))", w[0]): + found = True + + # now try to look for a # followed by any characters + if not found: + for w in reversed(word_list): + if re.match("#\S+", w[0]): + found = True + break + + if found: + issue = w[0] + start = w[1] + end = w[2] + if issue[0] == '#': + issue = issue[1:] + + return issue, start, end + + def getSeriesName(self, filename, issue_start ): + + # use the issue number string index to split the filename string + + if issue_start != 0: + filename = filename[:issue_start] + + # in case there is no issue number, remove some obvious stuff + if "--" in filename: + # the pattern seems to be that anything to left of the first "--" is the series name followed by issue + filename = re.sub("--.*", self.repl, filename) + + elif "__" in filename: + # the pattern seems to be that anything to left of the first "__" is the series name followed by issue + filename = re.sub("__.*", self.repl, filename) + + filename = filename.replace("+", " ") + tmpstr = self.fixSpaces(filename, remove_dashes=False) + + series = tmpstr + volume = "" + + #save the last word + try: + last_word = series.split()[-1] + except: + last_word = "" + + # remove any parenthetical phrases + series = re.sub( "\(.*?\)", "", series) + + # search for volume number + match = re.search('(.+)([vV]|[Vv][oO][Ll]\.?\s?)(\d+)\s*$', series) + if match: + series = match.group(1) + volume = match.group(3) + + # if a volume wasn't found, see if the last word is a year in parentheses + # since that's a common way to designate the volume + if volume == "": + #match either (YEAR), (YEAR-), or (YEAR-YEAR2) + match = re.search("(\()(\d{4})(-(\d{4}|)|)(\))", last_word) + if match: + volume = match.group(2) + + series = series.strip() + + # if we don't have an issue number (issue_start==0), look + # for hints i.e. "TPB", "one-shot", "OS", "OGN", etc that might + # be removed to help search online + if issue_start == 0: + one_shot_words = [ "tpb", "os", "one-shot", "ogn", "gn" ] + try: + last_word = series.split()[-1] + if last_word.lower() in one_shot_words: + series = series.rsplit(' ', 1)[0] + except: + pass + + return series, volume.strip() + + def getYear( self,filename, issue_end): + + filename = filename[issue_end:] + + year = "" + # look for four digit number with "(" ")" or "--" around it + match = re.search('(\(\d\d\d\d\))|(--\d\d\d\d--)', filename) + if match: + year = match.group() + # remove non-numerics + year = re.sub("[^0-9]", "", year) + return year + + def getRemainder( self, filename, year, count, issue_end ): + + #make a guess at where the the non-interesting stuff begins + remainder = "" + + if "--" in filename: + remainder = filename.split("--",1)[1] + elif "__" in filename: + remainder = filename.split("__",1)[1] + elif issue_end != 0: + remainder = filename[issue_end:] + + remainder = self.fixSpaces(remainder, remove_dashes=False) + if year != "": + remainder = remainder.replace(year,"",1) + if count != "": + remainder = remainder.replace("of "+count,"",1) + + remainder = remainder.replace("()","") + + return remainder.strip() + + def parseFilename( self, filename ): + + # remove the path + filename = os.path.basename(filename) + + # remove the extension + filename = os.path.splitext(filename)[0] + + #url decode, just in case + filename = unquote(filename) + + # sometimes archives get messed up names from too many decodings + # often url encodings will break and leave "_28" and "_29" in place + # of "(" and ")" see if there are a number of these, and replace them + if filename.count("_28") > 1 and filename.count("_29") > 1: + filename = filename.replace("_28", "(") + filename = filename.replace("_29", ")") + + self.issue, issue_start, issue_end = self.getIssueNumber(filename) + self.series, self.volume = self.getSeriesName(filename, issue_start) + self.year = self.getYear(filename, issue_end) + self.issue_count = self.getIssueCount(filename, issue_end) + self.remainder = self.getRemainder( filename, self.year, self.issue_count, issue_end ) + + if self.issue != "": + # strip off leading zeros + self.issue = self.issue.lstrip("0") + if self.issue == "": + self.issue = "0" + if self.issue[0] == ".": + self.issue = "0" + self.issue diff --git a/genericmetadata.py b/genericmetadata.py new file mode 100644 index 0000000..8e7aeaf --- /dev/null +++ b/genericmetadata.py @@ -0,0 +1,316 @@ +""" + A python class for internal metadata storage + + The goal of this class is to handle ALL the data that might come from various + tagging schemes and databases, such as ComicVine or GCD. This makes conversion + possible, however lossy it might be + +""" + +""" +Copyright 2012-2014 Anthony Beville + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +import utils + +# These page info classes are exactly the same as the CIX scheme, since it's unique +class PageType: + FrontCover = "FrontCover" + InnerCover = "InnerCover" + Roundup = "Roundup" + Story = "Story" + Advertisement = "Advertisement" + Editorial = "Editorial" + Letters = "Letters" + Preview = "Preview" + BackCover = "BackCover" + Other = "Other" + Deleted = "Deleted" + +""" +class PageInfo: + Image = 0 + Type = PageType.Story + DoublePage = False + ImageSize = 0 + Key = "" + ImageWidth = 0 + ImageHeight = 0 +""" + +class GenericMetadata: + + def __init__(self): + + self.isEmpty = True + self.tagOrigin = None + + self.series = None + self.issue = None + self.title = None + self.publisher = None + self.month = None + self.year = None + self.day = None + self.issueCount = None + self.volume = None + self.genre = None + self.language = None # 2 letter iso code + self.comments = None # use same way as Summary in CIX + + self.volumeCount = None + self.criticalRating = None + self.country = None + + self.alternateSeries = None + self.alternateNumber = None + self.alternateCount = None + self.imprint = None + self.notes = None + self.webLink = None + self.format = None + self.manga = None + self.blackAndWhite = None + self.pageCount = None + self.maturityRating = None + + self.storyArc = None + self.seriesGroup = None + self.scanInfo = None + + self.characters = None + self.teams = None + self.locations = None + + self.credits = list() + self.tags = list() + self.pages = list() + + # Some CoMet-only items + self.price = None + self.isVersionOf = None + self.rights = None + self.identifier = None + self.lastMark = None + self.coverImage = None + + def overlay( self, new_md ): + # Overlay a metadata object on this one + # that is, when the new object has non-None + # values, over-write them to this one + + def assign( cur, new ): + if new is not None: + if type(new) == str and len(new) == 0: + setattr(self, cur, None) + else: + setattr(self, cur, new) + + if not new_md.isEmpty: + self.isEmpty = False + + assign( 'series', new_md.series ) + assign( "issue", new_md.issue ) + assign( "issueCount", new_md.issueCount ) + assign( "title", new_md.title ) + assign( "publisher", new_md.publisher ) + assign( "day", new_md.day ) + assign( "month", new_md.month ) + assign( "year", new_md.year ) + assign( "volume", new_md.volume ) + assign( "volumeCount", new_md.volumeCount ) + assign( "genre", new_md.genre ) + assign( "language", new_md.language ) + assign( "country", new_md.country ) + assign( "criticalRating", new_md.criticalRating ) + assign( "alternateSeries", new_md.alternateSeries ) + assign( "alternateNumber", new_md.alternateNumber ) + assign( "alternateCount", new_md.alternateCount ) + assign( "imprint", new_md.imprint ) + assign( "webLink", new_md.webLink ) + assign( "format", new_md.format ) + assign( "manga", new_md.manga ) + assign( "blackAndWhite", new_md.blackAndWhite ) + assign( "maturityRating", new_md.maturityRating ) + assign( "storyArc", new_md.storyArc ) + assign( "seriesGroup", new_md.seriesGroup ) + assign( "scanInfo", new_md.scanInfo ) + assign( "characters", new_md.characters ) + assign( "teams", new_md.teams ) + assign( "locations", new_md.locations ) + assign( "comments", new_md.comments ) + assign( "notes", new_md.notes ) + + assign( "price", new_md.price ) + assign( "isVersionOf", new_md.isVersionOf ) + assign( "rights", new_md.rights ) + assign( "identifier", new_md.identifier ) + assign( "lastMark", new_md.lastMark ) + + self.overlayCredits( new_md.credits ) + # TODO + + # not sure if the tags and pages should broken down, or treated + # as whole lists.... + + # For now, go the easy route, where any overlay + # value wipes out the whole list + if len(new_md.tags) > 0: + assign( "tags", new_md.tags ) + + if len(new_md.pages) > 0: + assign( "pages", new_md.pages ) + + + def overlayCredits( self, new_credits ): + for c in new_credits: + if c.has_key('primary') and c['primary']: + primary = True + else: + primary = False + + # Remove credit role if person is blank + if c['person'] == "": + for r in reversed(self.credits): + if r['role'].lower() == c['role'].lower(): + self.credits.remove(r) + # otherwise, add it! + else: + self.addCredit( c['person'], c['role'], primary ) + + def setDefaultPageList( self, count ): + # generate a default page list, with the first page marked as the cover + for i in range(count): + page_dict = dict() + page_dict['Image'] = str(i) + if i == 0: + page_dict['Type'] = PageType.FrontCover + self.pages.append( page_dict ) + + def getArchivePageIndex( self, pagenum ): + # convert the displayed page number to the page index of the file in the archive + if pagenum < len( self.pages ): + return int( self.pages[pagenum]['Image'] ) + else: + return 0 + + def getCoverPageIndexList( self ): + # return a list of archive page indices of cover pages + coverlist = [] + for p in self.pages: + if 'Type' in p and p['Type'] == PageType.FrontCover: + coverlist.append( int(p['Image'])) + + if len(coverlist) == 0: + coverlist.append( 0 ) + + return coverlist + + def addCredit( self, person, role, primary = False ): + + credit = dict() + credit['person'] = person + credit['role'] = role + if primary: + credit['primary'] = primary + + # look to see if it's not already there... + found = False + for c in self.credits: + if ( c['person'].lower() == person.lower() and + c['role'].lower() == role.lower() ): + # no need to add it. just adjust the "primary" flag as needed + c['primary'] = primary + found = True + break + + if not found: + self.credits.append(credit) + + + def __str__( self ): + vals = [] + if self.isEmpty: + return "No metadata" + + def add_string( tag, val ): + if val is not None and u"{0}".format(val) != "": + vals.append( (tag, val) ) + + def add_attr_string( tag ): + val = getattr(self,tag) + add_string( tag, getattr(self,tag) ) + + add_attr_string( "series" ) + add_attr_string( "issue" ) + add_attr_string( "issueCount" ) + add_attr_string( "title" ) + add_attr_string( "publisher" ) + add_attr_string( "year" ) + add_attr_string( "month" ) + add_attr_string( "day" ) + add_attr_string( "volume" ) + add_attr_string( "volumeCount" ) + add_attr_string( "genre" ) + add_attr_string( "language" ) + add_attr_string( "country" ) + add_attr_string( "criticalRating" ) + add_attr_string( "alternateSeries" ) + add_attr_string( "alternateNumber" ) + add_attr_string( "alternateCount" ) + add_attr_string( "imprint" ) + add_attr_string( "webLink" ) + add_attr_string( "format" ) + add_attr_string( "manga" ) + + add_attr_string( "price" ) + add_attr_string( "isVersionOf" ) + add_attr_string( "rights" ) + add_attr_string( "identifier" ) + add_attr_string( "lastMark" ) + + if self.blackAndWhite: + add_attr_string( "blackAndWhite" ) + add_attr_string( "maturityRating" ) + add_attr_string( "storyArc" ) + add_attr_string( "seriesGroup" ) + add_attr_string( "scanInfo" ) + add_attr_string( "characters" ) + add_attr_string( "teams" ) + add_attr_string( "locations" ) + add_attr_string( "comments" ) + add_attr_string( "notes" ) + + add_string( "tags", utils.listToString( self.tags ) ) + + for c in self.credits: + primary = "" + if c.has_key('primary') and c['primary']: + primary = " [P]" + add_string( "credit", c['role']+": "+c['person'] + primary) + + # find the longest field name + flen = 0 + for i in vals: + flen = max( flen, len(i[0]) ) + flen += 1 + + #format the data nicely + outstr = "" + fmt_str = u"{0: <" + str(flen) + "} {1}\n" + for i in vals: + outstr += fmt_str.format( i[0]+":", i[1] ) + + return outstr diff --git a/issuestring.py b/issuestring.py new file mode 100644 index 0000000..751aa8c --- /dev/null +++ b/issuestring.py @@ -0,0 +1,140 @@ +# coding=utf-8 +""" +Class for handling the odd permutations of an 'issue number' that the comics industry throws at us + +e.g.: + +"12" +"12.1" +"0" +"-1" +"5AU" +"100-2" + +""" + +""" +Copyright 2012-2014 Anthony Beville + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +import utils +import math +import re + +class IssueString: + def __init__(self, text): + + # break up the issue number string into 2 parts: the numeric and suffix string. + # ( assumes that the numeric portion is always first ) + + self.num = None + self.suffix = "" + + if text is None: + return + + if type(text) == int: + text = str(text) + + if len(text) == 0: + return + + text = unicode(text) + + #skip the minus sign if it's first + if text[0] == '-': + start = 1 + else: + start = 0 + + # if it's still not numeric at start skip it + if text[start].isdigit() or text[start] == ".": + # walk through the string, look for split point (the first non-numeric) + decimal_count = 0 + for idx in range( start, len(text) ): + if text[idx] not in "0123456789.": + break + # special case: also split on second "." + if text[idx] == ".": + decimal_count += 1 + if decimal_count > 1: + break + else: + idx = len(text) + + # move trailing numeric decimal to suffix + # (only if there is other junk after ) + if text[idx-1] == "." and len(text) != idx: + idx = idx -1 + + # if there is no numeric after the minus, make the minus part of the suffix + if idx == 1 and start == 1: + idx = 0 + + part1 = text[0:idx] + part2 = text[idx:len(text)] + + if part1 != "": + self.num = float( part1 ) + self.suffix = part2 + else: + self.suffix = text + + #print "num: {0} suf: {1}".format(self.num, self.suffix) + + def asString( self, pad = 0 ): + #return the float, left side zero-padded, with suffix attached + if self.num is None: + return self.suffix + + negative = self.num < 0 + + num_f = abs(self.num) + + num_int = int( num_f ) + num_s = str( num_int ) + if float( num_int ) != num_f: + num_s = str( num_f ) + + num_s += self.suffix + + # create padding + padding = "" + l = len( str(num_int)) + if l < pad : + padding = "0" * (pad - l) + + num_s = padding + num_s + if negative: + num_s = "-" + num_s + + return num_s + + def asFloat( self ): + #return the float, with no suffix + if self.suffix == u"½": + if self.num is not None: + return self.num + .5 + else: + return .5 + return self.num + + def asInt( self ): + #return the int version of the float + if self.num is None: + return None + return int( self.num ) + + diff --git a/utils.py b/utils.py new file mode 100644 index 0000000..e315cd7 --- /dev/null +++ b/utils.py @@ -0,0 +1,597 @@ +# coding=utf-8 + +""" +Some generic utilities +""" + + +""" +Copyright 2012-2014 Anthony Beville + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" +import sys +import os +import re +import platform +import locale +import codecs + + +class UtilsVars: + already_fixed_encoding = False + +def get_actual_preferred_encoding(): + preferred_encoding = locale.getpreferredencoding() + if platform.system() == "Darwin": + preferred_encoding = "utf-8" + return preferred_encoding + +def fix_output_encoding( ): + if not UtilsVars.already_fixed_encoding: + # this reads the environment and inits the right locale + locale.setlocale(locale.LC_ALL, "") + + # try to make stdout/stderr encodings happy for unicode printing + preferred_encoding = get_actual_preferred_encoding() + sys.stdout = codecs.getwriter(preferred_encoding)(sys.stdout) + sys.stderr = codecs.getwriter(preferred_encoding)(sys.stderr) + UtilsVars.already_fixed_encoding = True + +def get_recursive_filelist( pathlist ): + """ + Get a recursive list of of all files under all path items in the list + """ + filename_encoding = sys.getfilesystemencoding() + filelist = [] + for p in pathlist: + # if path is a folder, walk it recursivly, and all files underneath + if type(p) == str: + #make sure string is unicode + p = p.decode(filename_encoding) #, 'replace') + elif type(p) != unicode: + #it's probably a QString + p = unicode(p) + + if os.path.isdir( p ): + for root,dirs,files in os.walk( p ): + for f in files: + if type(f) == str: + #make sure string is unicode + f = f.decode(filename_encoding, 'replace') + elif type(f) != unicode: + #it's probably a QString + f = unicode(f) + filelist.append(os.path.join(root,f)) + else: + filelist.append(p) + + return filelist + +def listToString( l ): + string = "" + if l is not None: + for item in l: + if len(string) > 0: + string += ", " + string += item + return string + +def addtopath( dirname ): + if dirname is not None and dirname != "": + + # verify that path doesn't already contain the given dirname + tmpdirname = re.escape(dirname) + pattern = r"{sep}{dir}$|^{dir}{sep}|{sep}{dir}{sep}|^{dir}$".format( dir=tmpdirname, sep=os.pathsep) + + match = re.search(pattern, os.environ['PATH']) + if not match: + os.environ['PATH'] = dirname + os.pathsep + os.environ['PATH'] + +# returns executable path, if it exists +def which(program): + + def is_exe(fpath): + return os.path.isfile(fpath) and os.access(fpath, os.X_OK) + + fpath, fname = os.path.split(program) + if fpath: + if is_exe(program): + return program + else: + for path in os.environ["PATH"].split(os.pathsep): + exe_file = os.path.join(path, program) + if is_exe(exe_file): + return exe_file + + return None + +def removearticles( text ): + text = text.lower() + articles = ['and', 'the', 'a', '&', 'issue' ] + newText = '' + for word in text.split(' '): + if word not in articles: + newText += word+' ' + + newText = newText[:-1] + + # now get rid of some other junk + newText = newText.replace(":", "") + newText = newText.replace(",", "") + newText = newText.replace("-", " ") + + # since the CV api changed, searches for series names with periods + # now explicity require the period to be in the search key, + # so the line below is removed (for now) + #newText = newText.replace(".", "") + + return newText + + +def unique_file(file_name): + counter = 1 + file_name_parts = os.path.splitext(file_name) # returns ('/path/file', '.ext') + while 1: + if not os.path.lexists( file_name): + return file_name + file_name = file_name_parts[0] + ' (' + str(counter) + ')' + file_name_parts[1] + counter += 1 + + +# -o- coding: utf-8 -o- +# ISO639 python dict +# oficial list in http://www.loc.gov/standards/iso639-2/php/code_list.php + +lang_dict = { + 'ab': 'Abkhaz', + 'aa': 'Afar', + 'af': 'Afrikaans', + 'ak': 'Akan', + 'sq': 'Albanian', + 'am': 'Amharic', + 'ar': 'Arabic', + 'an': 'Aragonese', + 'hy': 'Armenian', + 'as': 'Assamese', + 'av': 'Avaric', + 'ae': 'Avestan', + 'ay': 'Aymara', + 'az': 'Azerbaijani', + 'bm': 'Bambara', + 'ba': 'Bashkir', + 'eu': 'Basque', + 'be': 'Belarusian', + 'bn': 'Bengali', + 'bh': 'Bihari', + 'bi': 'Bislama', + 'bs': 'Bosnian', + 'br': 'Breton', + 'bg': 'Bulgarian', + 'my': 'Burmese', + 'ca': 'Catalan; Valencian', + 'ch': 'Chamorro', + 'ce': 'Chechen', + 'ny': 'Chichewa; Chewa; Nyanja', + 'zh': 'Chinese', + 'cv': 'Chuvash', + 'kw': 'Cornish', + 'co': 'Corsican', + 'cr': 'Cree', + 'hr': 'Croatian', + 'cs': 'Czech', + 'da': 'Danish', + 'dv': 'Divehi; Maldivian;', + 'nl': 'Dutch', + 'dz': 'Dzongkha', + 'en': 'English', + 'eo': 'Esperanto', + 'et': 'Estonian', + 'ee': 'Ewe', + 'fo': 'Faroese', + 'fj': 'Fijian', + 'fi': 'Finnish', + 'fr': 'French', + 'ff': 'Fula', + 'gl': 'Galician', + 'ka': 'Georgian', + 'de': 'German', + 'el': 'Greek, Modern', + 'gn': 'Guaraní', + 'gu': 'Gujarati', + 'ht': 'Haitian', + 'ha': 'Hausa', + 'he': 'Hebrew (modern)', + 'hz': 'Herero', + 'hi': 'Hindi', + 'ho': 'Hiri Motu', + 'hu': 'Hungarian', + 'ia': 'Interlingua', + 'id': 'Indonesian', + 'ie': 'Interlingue', + 'ga': 'Irish', + 'ig': 'Igbo', + 'ik': 'Inupiaq', + 'io': 'Ido', + 'is': 'Icelandic', + 'it': 'Italian', + 'iu': 'Inuktitut', + 'ja': 'Japanese', + 'jv': 'Javanese', + 'kl': 'Kalaallisut', + 'kn': 'Kannada', + 'kr': 'Kanuri', + 'ks': 'Kashmiri', + 'kk': 'Kazakh', + 'km': 'Khmer', + 'ki': 'Kikuyu, Gikuyu', + 'rw': 'Kinyarwanda', + 'ky': 'Kirghiz, Kyrgyz', + 'kv': 'Komi', + 'kg': 'Kongo', + 'ko': 'Korean', + 'ku': 'Kurdish', + 'kj': 'Kwanyama, Kuanyama', + 'la': 'Latin', + 'lb': 'Luxembourgish', + 'lg': 'Luganda', + 'li': 'Limburgish', + 'ln': 'Lingala', + 'lo': 'Lao', + 'lt': 'Lithuanian', + 'lu': 'Luba-Katanga', + 'lv': 'Latvian', + 'gv': 'Manx', + 'mk': 'Macedonian', + 'mg': 'Malagasy', + 'ms': 'Malay', + 'ml': 'Malayalam', + 'mt': 'Maltese', + 'mi': 'Māori', + 'mr': 'Marathi (Marāṭhī)', + 'mh': 'Marshallese', + 'mn': 'Mongolian', + 'na': 'Nauru', + 'nv': 'Navajo, Navaho', + 'nb': 'Norwegian Bokmål', + 'nd': 'North Ndebele', + 'ne': 'Nepali', + 'ng': 'Ndonga', + 'nn': 'Norwegian Nynorsk', + 'no': 'Norwegian', + 'ii': 'Nuosu', + 'nr': 'South Ndebele', + 'oc': 'Occitan', + 'oj': 'Ojibwe, Ojibwa', + 'cu': 'Old Church Slavonic', + 'om': 'Oromo', + 'or': 'Oriya', + 'os': 'Ossetian, Ossetic', + 'pa': 'Panjabi, Punjabi', + 'pi': 'Pāli', + 'fa': 'Persian', + 'pl': 'Polish', + 'ps': 'Pashto, Pushto', + 'pt': 'Portuguese', + 'qu': 'Quechua', + 'rm': 'Romansh', + 'rn': 'Kirundi', + 'ro': 'Romanian, Moldavan', + 'ru': 'Russian', + 'sa': 'Sanskrit (Saṁskṛta)', + 'sc': 'Sardinian', + 'sd': 'Sindhi', + 'se': 'Northern Sami', + 'sm': 'Samoan', + 'sg': 'Sango', + 'sr': 'Serbian', + 'gd': 'Scottish Gaelic', + 'sn': 'Shona', + 'si': 'Sinhala, Sinhalese', + 'sk': 'Slovak', + 'sl': 'Slovene', + 'so': 'Somali', + 'st': 'Southern Sotho', + 'es': 'Spanish; Castilian', + 'su': 'Sundanese', + 'sw': 'Swahili', + 'ss': 'Swati', + 'sv': 'Swedish', + 'ta': 'Tamil', + 'te': 'Telugu', + 'tg': 'Tajik', + 'th': 'Thai', + 'ti': 'Tigrinya', + 'bo': 'Tibetan', + 'tk': 'Turkmen', + 'tl': 'Tagalog', + 'tn': 'Tswana', + 'to': 'Tonga', + 'tr': 'Turkish', + 'ts': 'Tsonga', + 'tt': 'Tatar', + 'tw': 'Twi', + 'ty': 'Tahitian', + 'ug': 'Uighur, Uyghur', + 'uk': 'Ukrainian', + 'ur': 'Urdu', + 'uz': 'Uzbek', + 've': 'Venda', + 'vi': 'Vietnamese', + 'vo': 'Volapük', + 'wa': 'Walloon', + 'cy': 'Welsh', + 'wo': 'Wolof', + 'fy': 'Western Frisian', + 'xh': 'Xhosa', + 'yi': 'Yiddish', + 'yo': 'Yoruba', + 'za': 'Zhuang, Chuang', + 'zu': 'Zulu', +} + + +countries = [ + ('AF', 'Afghanistan'), + ('AL', 'Albania'), + ('DZ', 'Algeria'), + ('AS', 'American Samoa'), + ('AD', 'Andorra'), + ('AO', 'Angola'), + ('AI', 'Anguilla'), + ('AQ', 'Antarctica'), + ('AG', 'Antigua And Barbuda'), + ('AR', 'Argentina'), + ('AM', 'Armenia'), + ('AW', 'Aruba'), + ('AU', 'Australia'), + ('AT', 'Austria'), + ('AZ', 'Azerbaijan'), + ('BS', 'Bahamas'), + ('BH', 'Bahrain'), + ('BD', 'Bangladesh'), + ('BB', 'Barbados'), + ('BY', 'Belarus'), + ('BE', 'Belgium'), + ('BZ', 'Belize'), + ('BJ', 'Benin'), + ('BM', 'Bermuda'), + ('BT', 'Bhutan'), + ('BO', 'Bolivia'), + ('BA', 'Bosnia And Herzegowina'), + ('BW', 'Botswana'), + ('BV', 'Bouvet Island'), + ('BR', 'Brazil'), + ('BN', 'Brunei Darussalam'), + ('BG', 'Bulgaria'), + ('BF', 'Burkina Faso'), + ('BI', 'Burundi'), + ('KH', 'Cambodia'), + ('CM', 'Cameroon'), + ('CA', 'Canada'), + ('CV', 'Cape Verde'), + ('KY', 'Cayman Islands'), + ('CF', 'Central African Rep'), + ('TD', 'Chad'), + ('CL', 'Chile'), + ('CN', 'China'), + ('CX', 'Christmas Island'), + ('CC', 'Cocos Islands'), + ('CO', 'Colombia'), + ('KM', 'Comoros'), + ('CG', 'Congo'), + ('CK', 'Cook Islands'), + ('CR', 'Costa Rica'), + ('CI', 'Cote D`ivoire'), + ('HR', 'Croatia'), + ('CU', 'Cuba'), + ('CY', 'Cyprus'), + ('CZ', 'Czech Republic'), + ('DK', 'Denmark'), + ('DJ', 'Djibouti'), + ('DM', 'Dominica'), + ('DO', 'Dominican Republic'), + ('TP', 'East Timor'), + ('EC', 'Ecuador'), + ('EG', 'Egypt'), + ('SV', 'El Salvador'), + ('GQ', 'Equatorial Guinea'), + ('ER', 'Eritrea'), + ('EE', 'Estonia'), + ('ET', 'Ethiopia'), + ('FK', 'Falkland Islands (Malvinas)'), + ('FO', 'Faroe Islands'), + ('FJ', 'Fiji'), + ('FI', 'Finland'), + ('FR', 'France'), + ('GF', 'French Guiana'), + ('PF', 'French Polynesia'), + ('TF', 'French S. Territories'), + ('GA', 'Gabon'), + ('GM', 'Gambia'), + ('GE', 'Georgia'), + ('DE', 'Germany'), + ('GH', 'Ghana'), + ('GI', 'Gibraltar'), + ('GR', 'Greece'), + ('GL', 'Greenland'), + ('GD', 'Grenada'), + ('GP', 'Guadeloupe'), + ('GU', 'Guam'), + ('GT', 'Guatemala'), + ('GN', 'Guinea'), + ('GW', 'Guinea-bissau'), + ('GY', 'Guyana'), + ('HT', 'Haiti'), + ('HN', 'Honduras'), + ('HK', 'Hong Kong'), + ('HU', 'Hungary'), + ('IS', 'Iceland'), + ('IN', 'India'), + ('ID', 'Indonesia'), + ('IR', 'Iran'), + ('IQ', 'Iraq'), + ('IE', 'Ireland'), + ('IL', 'Israel'), + ('IT', 'Italy'), + ('JM', 'Jamaica'), + ('JP', 'Japan'), + ('JO', 'Jordan'), + ('KZ', 'Kazakhstan'), + ('KE', 'Kenya'), + ('KI', 'Kiribati'), + ('KP', 'Korea (North)'), + ('KR', 'Korea (South)'), + ('KW', 'Kuwait'), + ('KG', 'Kyrgyzstan'), + ('LA', 'Laos'), + ('LV', 'Latvia'), + ('LB', 'Lebanon'), + ('LS', 'Lesotho'), + ('LR', 'Liberia'), + ('LY', 'Libya'), + ('LI', 'Liechtenstein'), + ('LT', 'Lithuania'), + ('LU', 'Luxembourg'), + ('MO', 'Macau'), + ('MK', 'Macedonia'), + ('MG', 'Madagascar'), + ('MW', 'Malawi'), + ('MY', 'Malaysia'), + ('MV', 'Maldives'), + ('ML', 'Mali'), + ('MT', 'Malta'), + ('MH', 'Marshall Islands'), + ('MQ', 'Martinique'), + ('MR', 'Mauritania'), + ('MU', 'Mauritius'), + ('YT', 'Mayotte'), + ('MX', 'Mexico'), + ('FM', 'Micronesia'), + ('MD', 'Moldova'), + ('MC', 'Monaco'), + ('MN', 'Mongolia'), + ('MS', 'Montserrat'), + ('MA', 'Morocco'), + ('MZ', 'Mozambique'), + ('MM', 'Myanmar'), + ('NA', 'Namibia'), + ('NR', 'Nauru'), + ('NP', 'Nepal'), + ('NL', 'Netherlands'), + ('AN', 'Netherlands Antilles'), + ('NC', 'New Caledonia'), + ('NZ', 'New Zealand'), + ('NI', 'Nicaragua'), + ('NE', 'Niger'), + ('NG', 'Nigeria'), + ('NU', 'Niue'), + ('NF', 'Norfolk Island'), + ('MP', 'Northern Mariana Islands'), + ('NO', 'Norway'), + ('OM', 'Oman'), + ('PK', 'Pakistan'), + ('PW', 'Palau'), + ('PA', 'Panama'), + ('PG', 'Papua New Guinea'), + ('PY', 'Paraguay'), + ('PE', 'Peru'), + ('PH', 'Philippines'), + ('PN', 'Pitcairn'), + ('PL', 'Poland'), + ('PT', 'Portugal'), + ('PR', 'Puerto Rico'), + ('QA', 'Qatar'), + ('RE', 'Reunion'), + ('RO', 'Romania'), + ('RU', 'Russian Federation'), + ('RW', 'Rwanda'), + ('KN', 'Saint Kitts And Nevis'), + ('LC', 'Saint Lucia'), + ('VC', 'St Vincent/Grenadines'), + ('WS', 'Samoa'), + ('SM', 'San Marino'), + ('ST', 'Sao Tome'), + ('SA', 'Saudi Arabia'), + ('SN', 'Senegal'), + ('SC', 'Seychelles'), + ('SL', 'Sierra Leone'), + ('SG', 'Singapore'), + ('SK', 'Slovakia'), + ('SI', 'Slovenia'), + ('SB', 'Solomon Islands'), + ('SO', 'Somalia'), + ('ZA', 'South Africa'), + ('ES', 'Spain'), + ('LK', 'Sri Lanka'), + ('SH', 'St. Helena'), + ('PM', 'St.Pierre'), + ('SD', 'Sudan'), + ('SR', 'Suriname'), + ('SZ', 'Swaziland'), + ('SE', 'Sweden'), + ('CH', 'Switzerland'), + ('SY', 'Syrian Arab Republic'), + ('TW', 'Taiwan'), + ('TJ', 'Tajikistan'), + ('TZ', 'Tanzania'), + ('TH', 'Thailand'), + ('TG', 'Togo'), + ('TK', 'Tokelau'), + ('TO', 'Tonga'), + ('TT', 'Trinidad And Tobago'), + ('TN', 'Tunisia'), + ('TR', 'Turkey'), + ('TM', 'Turkmenistan'), + ('TV', 'Tuvalu'), + ('UG', 'Uganda'), + ('UA', 'Ukraine'), + ('AE', 'United Arab Emirates'), + ('UK', 'United Kingdom'), + ('US', 'United States'), + ('UY', 'Uruguay'), + ('UZ', 'Uzbekistan'), + ('VU', 'Vanuatu'), + ('VA', 'Vatican City State'), + ('VE', 'Venezuela'), + ('VN', 'Viet Nam'), + ('VG', 'Virgin Islands (British)'), + ('VI', 'Virgin Islands (U.S.)'), + ('EH', 'Western Sahara'), + ('YE', 'Yemen'), + ('YU', 'Yugoslavia'), + ('ZR', 'Zaire'), + ('ZM', 'Zambia'), + ('ZW', 'Zimbabwe') +] + + + +def getLanguageDict(): + return lang_dict + +def getLanguageFromISO( iso ): + if iso == None: + return None + else: + return lang_dict[ iso ] + + + + + + + + + + From 5d641af4f25962c6b7402054a388a279903651c0 Mon Sep 17 00:00:00 2001 From: Davide Romanini Date: Mon, 16 Feb 2015 14:05:02 +0100 Subject: [PATCH 02/22] using comicapi subtree classes --- comicarchive.py | 1953 ++++++++++++++++++++++++----------------------- 1 file changed, 1004 insertions(+), 949 deletions(-) diff --git a/comicarchive.py b/comicarchive.py index 381dc68..a2ef85c 100644 --- a/comicarchive.py +++ b/comicarchive.py @@ -9,7 +9,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -26,22 +26,66 @@ import tempfile import subprocess import platform import locale +import shutil + from natsort import natsorted +from unrar import rarfile +from unrar import unrarlib +import unrar.constants +import ctypes +import io +from unrar import constants + +class OpenableRarFile(rarfile.RarFile): + def open(self, member): + #print "opening %s..." % member + # based on https://github.com/matiasb/python-unrar/pull/4/files + res = [] + if isinstance(member, rarfile.RarInfo): + member = member.filename + archive = unrarlib.RAROpenArchiveDataEx(self.filename, mode=constants.RAR_OM_EXTRACT) + handle = self._open(archive) + found, buf = False, [] + def _callback(msg, UserData, P1, P2): + if msg == constants.UCM_PROCESSDATA: + data = (ctypes.c_char*P2).from_address(P1).raw + buf.append(data) + return 1 + c_callback = unrarlib.UNRARCALLBACK(_callback) + unrarlib.RARSetCallback(handle, c_callback, 1) + try: + rarinfo = self._read_header(handle) + while rarinfo is not None: + #print "checking rar archive %s against %s" % (rarinfo.filename, member) + if rarinfo.filename == member: + self._process_current(handle, constants.RAR_TEST) + found = True + else: + self._process_current(handle, constants.RAR_SKIP) + rarinfo = self._read_header(handle) + except unrarlib.UnrarException: + raise rarfile.BadRarFile("Bad RAR archive data.") + finally: + self._close(handle) + if not found: + raise KeyError('There is no item named %r in the archive' % member) + return ''.join(buf) + if platform.system() == "Windows": - import _subprocess + import _subprocess import time import StringIO try: - import Image - pil_available = True + import Image + pil_available = True except ImportError: - pil_available = False - + pil_available = False + sys.path.insert(0, os.path.abspath(".") ) -import UnRAR2 -from UnRAR2.rar_exceptions import * +#import UnRAR2 +#from UnRAR2.rar_exceptions import * #from settings import ComicTaggerSettings from comicinfoxml import ComicInfoXml @@ -52,1037 +96,1048 @@ from filenameparser import FileNameParser from PyPDF2 import PdfFileReader class MetaDataStyle: - CBI = 0 - CIX = 1 - COMET = 2 - name = [ 'ComicBookLover', 'ComicRack', 'CoMet' ] + CBI = 0 + CIX = 1 + COMET = 2 + name = [ 'ComicBookLover', 'ComicRack', 'CoMet' ] class ZipArchiver: - - def __init__( self, path ): - self.path = path - - def getArchiveComment( self ): - zf = zipfile.ZipFile( self.path, 'r' ) - comment = zf.comment - zf.close() - return comment - def setArchiveComment( self, comment ): - return self.writeZipComment( self.path, comment ) + def __init__( self, path ): + self.path = path - def readArchiveFile( self, archive_file ): - data = "" - zf = zipfile.ZipFile( self.path, 'r' ) + def getArchiveComment( self ): + zf = zipfile.ZipFile( self.path, 'r' ) + comment = zf.comment + zf.close() + return comment - try: - data = zf.read( archive_file ) - except zipfile.BadZipfile as e: - print >> sys.stderr, u"bad zipfile [{0}]: {1} :: {2}".format(e, self.path, archive_file) - zf.close() - raise IOError - except Exception as e: - zf.close() - print >> sys.stderr, u"bad zipfile [{0}]: {1} :: {2}".format(e, self.path, archive_file) - raise IOError - finally: - zf.close() - return data + def setArchiveComment( self, comment ): + return self.writeZipComment( self.path, comment ) - def removeArchiveFile( self, archive_file ): - try: - self.rebuildZipFile( [ archive_file ] ) - except: - return False - else: - return True - - def writeArchiveFile( self, archive_file, data ): - # At the moment, no other option but to rebuild the whole - # zip archive w/o the indicated file. Very sucky, but maybe - # another solution can be found - try: - self.rebuildZipFile( [ archive_file ] ) - - #now just add the archive file as a new one - zf = zipfile.ZipFile(self.path, mode='a', compression=zipfile.ZIP_DEFLATED ) - zf.writestr( archive_file, data ) - zf.close() - return True - except: - return False - - def getArchiveFilenameList( self ): - try: - zf = zipfile.ZipFile( self.path, 'r' ) - namelist = zf.namelist() - zf.close() - return namelist - except Exception as e: - print >> sys.stderr, u"Unable to get zipfile list [{0}]: {1}".format(e, self.path) - return [] - - # zip helper func - def rebuildZipFile( self, exclude_list ): - - # this recompresses the zip archive, without the files in the exclude_list - #print ">> sys.stderr, Rebuilding zip {0} without {1}".format( self.path, exclude_list ) - - # generate temp file - tmp_fd, tmp_name = tempfile.mkstemp( dir=os.path.dirname(self.path) ) - os.close( tmp_fd ) - - zin = zipfile.ZipFile (self.path, 'r') - zout = zipfile.ZipFile (tmp_name, 'w') - for item in zin.infolist(): - buffer = zin.read(item.filename) - if ( item.filename not in exclude_list ): - zout.writestr(item, buffer) - - #preserve the old comment - zout.comment = zin.comment - - zout.close() - zin.close() - - # replace with the new file - os.remove( self.path ) - os.rename( tmp_name, self.path ) + def readArchiveFile( self, archive_file ): + data = "" + zf = zipfile.ZipFile( self.path, 'r' ) + + try: + data = zf.read( archive_file ) + except zipfile.BadZipfile as e: + print >> sys.stderr, u"bad zipfile [{0}]: {1} :: {2}".format(e, self.path, archive_file) + zf.close() + raise IOError + except Exception as e: + zf.close() + print >> sys.stderr, u"bad zipfile [{0}]: {1} :: {2}".format(e, self.path, archive_file) + raise IOError + finally: + zf.close() + return data + + def removeArchiveFile( self, archive_file ): + try: + self.rebuildZipFile( [ archive_file ] ) + except: + return False + else: + return True + + def writeArchiveFile( self, archive_file, data ): + # At the moment, no other option but to rebuild the whole + # zip archive w/o the indicated file. Very sucky, but maybe + # another solution can be found + try: + self.rebuildZipFile( [ archive_file ] ) + + #now just add the archive file as a new one + zf = zipfile.ZipFile(self.path, mode='a', compression=zipfile.ZIP_DEFLATED ) + zf.writestr( archive_file, data ) + zf.close() + return True + except: + return False + + def getArchiveFilenameList( self ): + try: + zf = zipfile.ZipFile( self.path, 'r' ) + namelist = zf.namelist() + zf.close() + return namelist + except Exception as e: + print >> sys.stderr, u"Unable to get zipfile list [{0}]: {1}".format(e, self.path) + return [] + + # zip helper func + def rebuildZipFile( self, exclude_list ): + + # this recompresses the zip archive, without the files in the exclude_list + #print ">> sys.stderr, Rebuilding zip {0} without {1}".format( self.path, exclude_list ) + + # generate temp file + tmp_fd, tmp_name = tempfile.mkstemp( dir=os.path.dirname(self.path) ) + os.close( tmp_fd ) + + zin = zipfile.ZipFile (self.path, 'r') + zout = zipfile.ZipFile (tmp_name, 'w') + for item in zin.infolist(): + buffer = zin.read(item.filename) + if ( item.filename not in exclude_list ): + zout.writestr(item, buffer) + + #preserve the old comment + zout.comment = zin.comment + + zout.close() + zin.close() + + # replace with the new file + os.remove( self.path ) + os.rename( tmp_name, self.path ) - def writeZipComment( self, filename, comment ): - """ - This is a custom function for writing a comment to a zip file, - since the built-in one doesn't seem to work on Windows and Mac OS/X + def writeZipComment( self, filename, comment ): + """ + This is a custom function for writing a comment to a zip file, + since the built-in one doesn't seem to work on Windows and Mac OS/X - Fortunately, the zip comment is at the end of the file, and it's - easy to manipulate. See this website for more info: - see: http://en.wikipedia.org/wiki/Zip_(file_format)#Structure - """ + Fortunately, the zip comment is at the end of the file, and it's + easy to manipulate. See this website for more info: + see: http://en.wikipedia.org/wiki/Zip_(file_format)#Structure + """ - #get file size - statinfo = os.stat(filename) - file_length = statinfo.st_size + #get file size + statinfo = os.stat(filename) + file_length = statinfo.st_size - try: - fo = open(filename, "r+b") + try: + fo = open(filename, "r+b") - #the starting position, relative to EOF - pos = -4 + #the starting position, relative to EOF + pos = -4 - found = False - value = bytearray() - - # walk backwards to find the "End of Central Directory" record - while ( not found ) and ( -pos != file_length ): - # seek, relative to EOF - fo.seek( pos, 2) + found = False + value = bytearray() - value = fo.read( 4 ) + # walk backwards to find the "End of Central Directory" record + while ( not found ) and ( -pos != file_length ): + # seek, relative to EOF + fo.seek( pos, 2) - #look for the end of central directory signature - if bytearray(value) == bytearray([ 0x50, 0x4b, 0x05, 0x06 ]): - found = True - else: - # not found, step back another byte - pos = pos - 1 - #print pos,"{1} int: {0:x}".format(bytearray(value)[0], value) - - if found: - - # now skip forward 20 bytes to the comment length word - pos += 20 - fo.seek( pos, 2) + value = fo.read( 4 ) - # Pack the length of the comment string - format = "H" # one 2-byte integer - comment_length = struct.pack(format, len(comment)) # pack integer in a binary string - - # write out the length - fo.write( comment_length ) - fo.seek( pos+2, 2) - - # write out the comment itself - fo.write( comment ) - fo.truncate() - fo.close() - else: - raise Exception('Failed to write comment to zip file!') - except: - return False - else: - return True + #look for the end of central directory signature + if bytearray(value) == bytearray([ 0x50, 0x4b, 0x05, 0x06 ]): + found = True + else: + # not found, step back another byte + pos = pos - 1 + #print pos,"{1} int: {0:x}".format(bytearray(value)[0], value) + + if found: + + # now skip forward 20 bytes to the comment length word + pos += 20 + fo.seek( pos, 2) + + # Pack the length of the comment string + format = "H" # one 2-byte integer + comment_length = struct.pack(format, len(comment)) # pack integer in a binary string + + # write out the length + fo.write( comment_length ) + fo.seek( pos+2, 2) + + # write out the comment itself + fo.write( comment ) + fo.truncate() + fo.close() + else: + raise Exception('Failed to write comment to zip file!') + except: + return False + else: + return True + + def copyFromArchive( self, otherArchive ): + # Replace the current zip with one copied from another archive + try: + zout = zipfile.ZipFile (self.path, 'w') + for fname in otherArchive.getArchiveFilenameList(): + data = otherArchive.readArchiveFile( fname ) + if data is not None: + zout.writestr( fname, data ) + zout.close() + + #preserve the old comment + comment = otherArchive.getArchiveComment() + if comment is not None: + if not self.writeZipComment( self.path, comment ): + return False + except Exception as e: + print >> sys.stderr, u"Error while copying to {0}: {1}".format(self.path, e) + return False + else: + return True - def copyFromArchive( self, otherArchive ): - # Replace the current zip with one copied from another archive - try: - zout = zipfile.ZipFile (self.path, 'w') - for fname in otherArchive.getArchiveFilenameList(): - data = otherArchive.readArchiveFile( fname ) - if data is not None: - zout.writestr( fname, data ) - zout.close() - - #preserve the old comment - comment = otherArchive.getArchiveComment() - if comment is not None: - if not self.writeZipComment( self.path, comment ): - return False - except Exception as e: - print >> sys.stderr, u"Error while copying to {0}: {1}".format(self.path, e) - return False - else: - return True - #------------------------------------------ # RAR implementation - + class RarArchiver: - - devnull = None - def __init__( self, path, rar_exe_path ): - self.path = path - self.rar_exe_path = rar_exe_path - if RarArchiver.devnull is None: - RarArchiver.devnull = open(os.devnull, "w") + devnull = None + def __init__( self, path, rar_exe_path ): + self.path = path + self.rar_exe_path = rar_exe_path - # windows only, keeps the cmd.exe from popping up - if platform.system() == "Windows": - self.startupinfo = subprocess.STARTUPINFO() - self.startupinfo.dwFlags |= _subprocess.STARTF_USESHOWWINDOW - else: - self.startupinfo = None + if RarArchiver.devnull is None: + RarArchiver.devnull = open(os.devnull, "w") - def __del__(self): - #RarArchiver.devnull.close() - pass + # windows only, keeps the cmd.exe from popping up + if platform.system() == "Windows": + self.startupinfo = subprocess.STARTUPINFO() + self.startupinfo.dwFlags |= _subprocess.STARTF_USESHOWWINDOW + else: + self.startupinfo = None - def getArchiveComment( self ): - - rarc = self.getRARObj() - return rarc.comment + def __del__(self): + #RarArchiver.devnull.close() + pass - def setArchiveComment( self, comment ): + def getArchiveComment( self ): - if self.rar_exe_path is not None: - try: - # write comment to temp file - tmp_fd, tmp_name = tempfile.mkstemp() - f = os.fdopen(tmp_fd, 'w+b') - f.write( comment ) - f.close() + rarc = self.getRARObj() + return rarc.comment - working_dir = os.path.dirname( os.path.abspath( self.path ) ) + def setArchiveComment( self, comment ): - # use external program to write comment to Rar archive - subprocess.call([self.rar_exe_path, 'c', '-w' + working_dir , '-c-', '-z' + tmp_name, self.path], - startupinfo=self.startupinfo, - stdout=RarArchiver.devnull) - - if platform.system() == "Darwin": - time.sleep(1) - - os.remove( tmp_name) - except: - return False - else: - return True - else: - return False - - def readArchiveFile( self, archive_file ): + if self.rar_exe_path is not None: + try: + # write comment to temp file + tmp_fd, tmp_name = tempfile.mkstemp() + f = os.fdopen(tmp_fd, 'w+b') + f.write( comment ) + f.close() - # Make sure to escape brackets, since some funky stuff is going on - # underneath with "fnmatch" - archive_file = archive_file.replace("[", '[[]') - entries = [] + working_dir = os.path.dirname( os.path.abspath( self.path ) ) - rarc = self.getRARObj() + # use external program to write comment to Rar archive + subprocess.call([self.rar_exe_path, 'c', '-w' + working_dir , '-c-', '-z' + tmp_name, self.path], + startupinfo=self.startupinfo, + stdout=RarArchiver.devnull) - tries = 0 - while tries < 7: - try: - tries = tries+1 - entries = rarc.read_files( archive_file ) + if platform.system() == "Darwin": + time.sleep(1) - if entries[0][0].size != len(entries[0][1]): - print >> sys.stderr, u"readArchiveFile(): [file is not expected size: {0} vs {1}] {2}:{3} [attempt # {4}]".format( - entries[0][0].size,len(entries[0][1]), self.path, archive_file, tries) - continue - - except (OSError, IOError) as e: - print >> sys.stderr, u"readArchiveFile(): [{0}] {1}:{2} attempt#{3}".format(str(e), self.path, archive_file, tries) - time.sleep(1) - except Exception as e: - print >> sys.stderr, u"Unexpected exception in readArchiveFile(): [{0}] for {1}:{2} attempt#{3}".format(str(e), self.path, archive_file, tries) - break + os.remove( tmp_name) + except: + return False + else: + return True + else: + return False - else: - #Success" - #entries is a list of of tuples: ( rarinfo, filedata) - if tries > 1: - print >> sys.stderr, u"Attempted read_files() {0} times".format(tries) - if (len(entries) == 1): - return entries[0][1] - else: - raise IOError - - raise IOError - + def readArchiveFile( self, archive_file ): - - def writeArchiveFile( self, archive_file, data ): + # Make sure to escape brackets, since some funky stuff is going on + # underneath with "fnmatch" + #archive_file = archive_file.replace("[", '[[]') + entries = [] - if self.rar_exe_path is not None: - try: - tmp_folder = tempfile.mkdtemp() + rarc = self.getRARObj() - tmp_file = os.path.join( tmp_folder, archive_file ) - - working_dir = os.path.dirname( os.path.abspath( self.path ) ) + tries = 0 + while tries < 7: + try: + tries = tries+1 + #tmp_folder = tempfile.mkdtemp() + #tmp_file = os.path.join(tmp_folder, archive_file) + #rarc.extract(archive_file, tmp_folder) + data = rarc.open(archive_file) + #data = open(tmp_file).read() + entries = [(rarc.getinfo(archive_file), data)] - # TODO: will this break if 'archive_file' is in a subfolder. i.e. "foo/bar.txt" - # will need to create the subfolder above, I guess... - f = open(tmp_file, 'w') - f.write( data ) - f.close() - - # use external program to write file to Rar archive - subprocess.call([self.rar_exe_path, 'a', '-w' + working_dir ,'-c-', '-ep', self.path, tmp_file], - startupinfo=self.startupinfo, - stdout=RarArchiver.devnull) - if platform.system() == "Darwin": - time.sleep(1) - os.remove( tmp_file) - os.rmdir( tmp_folder) - except: - return False - else: - return True - else: - return False - - def removeArchiveFile( self, archive_file ): - if self.rar_exe_path is not None: - try: - # use external program to remove file from Rar archive - subprocess.call([self.rar_exe_path, 'd','-c-', self.path, archive_file], - startupinfo=self.startupinfo, - stdout=RarArchiver.devnull) + #shutil.rmtree(tmp_folder, ignore_errors=True) - if platform.system() == "Darwin": - time.sleep(1) - except: - return False - else: - return True - else: - return False - - def getArchiveFilenameList( self ): + #entries = rarc.read_files( archive_file ) - rarc = self.getRARObj() - #namelist = [ item.filename for item in rarc.infolist() ] - #return namelist + if entries[0][0].file_size != len(entries[0][1]): + print >> sys.stderr, u"readArchiveFile(): [file is not expected size: {0} vs {1}] {2}:{3} [attempt # {4}]".format( + entries[0][0].file_size,len(entries[0][1]), self.path, archive_file, tries) + continue - tries = 0 - while tries < 7: - try: - tries = tries+1 - #namelist = [ item.filename for item in rarc.infolist() ] - namelist = [] - for item in rarc.infolist(): - if item.size != 0: - namelist.append( item.filename ) - - except (OSError, IOError) as e: - print >> sys.stderr, u"getArchiveFilenameList(): [{0}] {1} attempt#{2}".format(str(e), self.path, tries) - time.sleep(1) + except (OSError, IOError) as e: + print >> sys.stderr, u"readArchiveFile(): [{0}] {1}:{2} attempt#{3}".format(str(e), self.path, archive_file, tries) + time.sleep(1) + except Exception as e: + print >> sys.stderr, u"Unexpected exception in readArchiveFile(): [{0}] for {1}:{2} attempt#{3}".format(str(e), self.path, archive_file, tries) + break - else: - #Success" - return namelist - - raise e - - - def getRARObj( self ): - tries = 0 - while tries < 7: - try: - tries = tries+1 - rarc = UnRAR2.RarFile( self.path ) - - except (OSError, IOError) as e: - print >> sys.stderr, u"getRARObj(): [{0}] {1} attempt#{2}".format(str(e), self.path, tries) - time.sleep(1) + else: + #Success" + #entries is a list of of tuples: ( rarinfo, filedata) + if tries > 1: + print >> sys.stderr, u"Attempted read_files() {0} times".format(tries) + if (len(entries) == 1): + return entries[0][1] + else: + raise IOError + + raise IOError + + + + def writeArchiveFile( self, archive_file, data ): + + if self.rar_exe_path is not None: + try: + tmp_folder = tempfile.mkdtemp() + + tmp_file = os.path.join( tmp_folder, archive_file ) + + working_dir = os.path.dirname( os.path.abspath( self.path ) ) + + # TODO: will this break if 'archive_file' is in a subfolder. i.e. "foo/bar.txt" + # will need to create the subfolder above, I guess... + f = open(tmp_file, 'w') + f.write( data ) + f.close() + + # use external program to write file to Rar archive + subprocess.call([self.rar_exe_path, 'a', '-w' + working_dir ,'-c-', '-ep', self.path, tmp_file], + startupinfo=self.startupinfo, + stdout=RarArchiver.devnull) + + if platform.system() == "Darwin": + time.sleep(1) + os.remove( tmp_file) + os.rmdir( tmp_folder) + except: + return False + else: + return True + else: + return False + + def removeArchiveFile( self, archive_file ): + if self.rar_exe_path is not None: + try: + # use external program to remove file from Rar archive + subprocess.call([self.rar_exe_path, 'd','-c-', self.path, archive_file], + startupinfo=self.startupinfo, + stdout=RarArchiver.devnull) + + if platform.system() == "Darwin": + time.sleep(1) + except: + return False + else: + return True + else: + return False + + def getArchiveFilenameList( self ): + + rarc = self.getRARObj() + #namelist = [ item.filename for item in rarc.infolist() ] + #return namelist + + tries = 0 + while tries < 7: + try: + tries = tries+1 + #namelist = [ item.filename for item in rarc.infolist() ] + namelist = [] + for item in rarc.infolist(): + if item.file_size != 0: + namelist.append( item.filename ) + + except (OSError, IOError) as e: + print >> sys.stderr, u"getArchiveFilenameList(): [{0}] {1} attempt#{2}".format(str(e), self.path, tries) + time.sleep(1) + + else: + #Success" + return namelist + + raise e + + + def getRARObj( self ): + tries = 0 + while tries < 7: + try: + tries = tries+1 + #rarc = UnRAR2.RarFile( self.path ) + rarc = OpenableRarFile(self.path) + + except (OSError, IOError) as e: + print >> sys.stderr, u"getRARObj(): [{0}] {1} attempt#{2}".format(str(e), self.path, tries) + time.sleep(1) + + else: + #Success" + return rarc + + raise e - else: - #Success" - return rarc - - raise e - #------------------------------------------ # Folder implementation class FolderArchiver: - - def __init__( self, path ): - self.path = path - self.comment_file_name = "ComicTaggerFolderComment.txt" - def getArchiveComment( self ): - return self.readArchiveFile( self.comment_file_name ) - - def setArchiveComment( self, comment ): - return self.writeArchiveFile( self.comment_file_name, comment ) - - def readArchiveFile( self, archive_file ): - - data = "" - fname = os.path.join( self.path, archive_file ) - try: - with open( fname, 'rb' ) as f: - data = f.read() - f.close() - except IOError as e: - pass - - return data + def __init__( self, path ): + self.path = path + self.comment_file_name = "ComicTaggerFolderComment.txt" - def writeArchiveFile( self, archive_file, data ): + def getArchiveComment( self ): + return self.readArchiveFile( self.comment_file_name ) - fname = os.path.join( self.path, archive_file ) - try: - with open(fname, 'w+') as f: - f.write( data ) - f.close() - except: - return False - else: - return True - - def removeArchiveFile( self, archive_file ): + def setArchiveComment( self, comment ): + return self.writeArchiveFile( self.comment_file_name, comment ) - fname = os.path.join( self.path, archive_file ) - try: - os.remove( fname ) - except: - return False - else: - return True - - def getArchiveFilenameList( self ): - return self.listFiles( self.path ) - - def listFiles( self, folder ): - - itemlist = list() + def readArchiveFile( self, archive_file ): - for item in os.listdir( folder ): - itemlist.append( item ) - if os.path.isdir( item ): - itemlist.extend( self.listFiles( os.path.join( folder, item ) )) + data = "" + fname = os.path.join( self.path, archive_file ) + try: + with open( fname, 'rb' ) as f: + data = f.read() + f.close() + except IOError as e: + pass - return itemlist + return data + + def writeArchiveFile( self, archive_file, data ): + + fname = os.path.join( self.path, archive_file ) + try: + with open(fname, 'w+') as f: + f.write( data ) + f.close() + except: + return False + else: + return True + + def removeArchiveFile( self, archive_file ): + + fname = os.path.join( self.path, archive_file ) + try: + os.remove( fname ) + except: + return False + else: + return True + + def getArchiveFilenameList( self ): + return self.listFiles( self.path ) + + def listFiles( self, folder ): + + itemlist = list() + + for item in os.listdir( folder ): + itemlist.append( item ) + if os.path.isdir( item ): + itemlist.extend( self.listFiles( os.path.join( folder, item ) )) + + return itemlist #------------------------------------------ # Unknown implementation class UnknownArchiver: - - def __init__( self, path ): - self.path = path - def getArchiveComment( self ): - return "" - def setArchiveComment( self, comment ): - return False - def readArchiveFile( self ): - return "" - def writeArchiveFile( self, archive_file, data ): - return False - def removeArchiveFile( self, archive_file ): - return False - def getArchiveFilenameList( self ): - return [] + def __init__( self, path ): + self.path = path + + def getArchiveComment( self ): + return "" + def setArchiveComment( self, comment ): + return False + def readArchiveFile( self ): + return "" + def writeArchiveFile( self, archive_file, data ): + return False + def removeArchiveFile( self, archive_file ): + return False + def getArchiveFilenameList( self ): + return [] class PdfArchiver: - def __init__( self, path ): - self.path = path + def __init__( self, path ): + self.path = path - def getArchiveComment( self ): - return "" - def setArchiveComment( self, comment ): - return False - def readArchiveFile( self, page_num ): - return subprocess.check_output(['mudraw', '-o','-', self.path, str(int(os.path.basename(page_num)[:-4]))]) - def writeArchiveFile( self, archive_file, data ): - return False - def removeArchiveFile( self, archive_file ): - return False - def getArchiveFilenameList( self ): - out = [] - pdf = PdfFileReader(open(self.path, 'rb')) - for page in range(1, pdf.getNumPages() + 1): - out.append("/%04d.jpg" % (page)) - return out + def getArchiveComment( self ): + return "" + def setArchiveComment( self, comment ): + return False + def readArchiveFile( self, page_num ): + return subprocess.check_output(['mudraw', '-o','-', self.path, str(int(os.path.basename(page_num)[:-4]))]) + def writeArchiveFile( self, archive_file, data ): + return False + def removeArchiveFile( self, archive_file ): + return False + def getArchiveFilenameList( self ): + out = [] + pdf = PdfFileReader(open(self.path, 'rb')) + for page in range(1, pdf.getNumPages() + 1): + out.append("/%04d.jpg" % (page)) + return out #------------------------------------------------------------------ class ComicArchive: - logo_data = None + logo_data = None - class ArchiveType: - Zip, Rar, Folder, Pdf, Unknown = range(5) + class ArchiveType: + Zip, Rar, Folder, Pdf, Unknown = range(5) - def __init__( self, path, rar_exe_path=None, default_image_path=None ): - self.path = path - - self.rar_exe_path = rar_exe_path - self.ci_xml_filename = 'ComicInfo.xml' - self.comet_default_filename = 'CoMet.xml' - self.resetCache() - self.default_image_path = default_image_path - - # Use file extension to decide which archive test we do first - ext = os.path.splitext(path)[1].lower() - - self.archive_type = self.ArchiveType.Unknown - self.archiver = UnknownArchiver( self.path ) + def __init__( self, path, rar_exe_path=None, default_image_path=None ): + self.path = path - if ext == ".cbr" or ext == ".rar": - if self.rarTest(): - self.archive_type = self.ArchiveType.Rar - self.archiver = RarArchiver( self.path, rar_exe_path=self.rar_exe_path ) - - elif self.zipTest(): - self.archive_type = self.ArchiveType.Zip - self.archiver = ZipArchiver( self.path ) - else: - if self.zipTest(): - self.archive_type = self.ArchiveType.Zip - self.archiver = ZipArchiver( self.path ) - - elif self.rarTest(): - self.archive_type = self.ArchiveType.Rar - self.archiver = RarArchiver( self.path, rar_exe_path=self.rar_exe_path ) - elif os.path.basename(self.path)[-3:] == 'pdf': - self.archive_type = self.ArchiveType.Pdf - self.archiver = PdfArchiver(self.path) + self.rar_exe_path = rar_exe_path + self.ci_xml_filename = 'ComicInfo.xml' + self.comet_default_filename = 'CoMet.xml' + self.resetCache() + self.default_image_path = default_image_path - if ComicArchive.logo_data is None: - #fname = ComicTaggerSettings.getGraphic('nocover.png') - fname = self.default_image_path - with open(fname, 'rb') as fd: - ComicArchive.logo_data = fd.read() + # Use file extension to decide which archive test we do first + ext = os.path.splitext(path)[1].lower() - # Clears the cached data - def resetCache( self ): - self.has_cix = None - self.has_cbi = None - self.has_comet = None - self.comet_filename = None - self.page_count = None - self.page_list = None - self.cix_md = None - self.cbi_md = None - self.comet_md = None + self.archive_type = self.ArchiveType.Unknown + self.archiver = UnknownArchiver( self.path ) - def loadCache( self, style_list ): - for style in style_list: - self.readMetadata(style) - - def rename( self, path ): - self.path = path - self.archiver.path = path + if ext == ".cbr" or ext == ".rar": + if self.rarTest(): + self.archive_type = self.ArchiveType.Rar + self.archiver = RarArchiver( self.path, rar_exe_path=self.rar_exe_path ) - def zipTest( self ): - return zipfile.is_zipfile( self.path ) + elif self.zipTest(): + self.archive_type = self.ArchiveType.Zip + self.archiver = ZipArchiver( self.path ) + else: + if self.zipTest(): + self.archive_type = self.ArchiveType.Zip + self.archiver = ZipArchiver( self.path ) - def rarTest( self ): - try: - rarc = UnRAR2.RarFile( self.path ) - except: # InvalidRARArchive: - return False - else: - return True + elif self.rarTest(): + self.archive_type = self.ArchiveType.Rar + self.archiver = RarArchiver( self.path, rar_exe_path=self.rar_exe_path ) + elif os.path.basename(self.path)[-3:] == 'pdf': + self.archive_type = self.ArchiveType.Pdf + self.archiver = PdfArchiver(self.path) - - def isZip( self ): - return self.archive_type == self.ArchiveType.Zip - - def isRar( self ): - return self.archive_type == self.ArchiveType.Rar - def isPdf(self): - return self.archive_type == self.ArchiveType.Pdf - def isFolder( self ): - return self.archive_type == self.ArchiveType.Folder + if ComicArchive.logo_data is None: + #fname = ComicTaggerSettings.getGraphic('nocover.png') + fname = self.default_image_path + with open(fname, 'rb') as fd: + ComicArchive.logo_data = fd.read() - def isWritable( self, check_rar_status=True ): - if self.archive_type == self.ArchiveType.Unknown : - return False - - elif check_rar_status and self.isRar() and self.rar_exe_path is None: - return False - - elif not os.access(self.path, os.W_OK): - return False - - elif ((self.archive_type != self.ArchiveType.Folder) and - (not os.access( os.path.dirname( os.path.abspath(self.path)), os.W_OK ))): - return False + # Clears the cached data + def resetCache( self ): + self.has_cix = None + self.has_cbi = None + self.has_comet = None + self.comet_filename = None + self.page_count = None + self.page_list = None + self.cix_md = None + self.cbi_md = None + self.comet_md = None - return True + def loadCache( self, style_list ): + for style in style_list: + self.readMetadata(style) - def isWritableForStyle( self, data_style ): + def rename( self, path ): + self.path = path + self.archiver.path = path - if self.isRar() and data_style == MetaDataStyle.CBI: - return False + def zipTest( self ): + return zipfile.is_zipfile( self.path ) - return self.isWritable() - - def seemsToBeAComicArchive( self ): - - # Do we even care about extensions?? - ext = os.path.splitext(self.path)[1].lower() - - if ( - ( self.isZip() or self.isRar() or self.isPdf()) #or self.isFolder() ) - and - ( self.getNumberOfPages() > 0) - - ): - return True - else: - return False - - def readMetadata( self, style ): - - if style == MetaDataStyle.CIX: - return self.readCIX() - elif style == MetaDataStyle.CBI: - return self.readCBI() - elif style == MetaDataStyle.COMET: - return self.readCoMet() - else: - return GenericMetadata() - - def writeMetadata( self, metadata, style ): - - retcode = None - if style == MetaDataStyle.CIX: - retcode = self.writeCIX( metadata ) - elif style == MetaDataStyle.CBI: - retcode = self.writeCBI( metadata ) - elif style == MetaDataStyle.COMET: - retcode = self.writeCoMet( metadata ) - return retcode - - - def hasMetadata( self, style ): - - if style == MetaDataStyle.CIX: - return self.hasCIX() - elif style == MetaDataStyle.CBI: - return self.hasCBI() - elif style == MetaDataStyle.COMET: - return self.hasCoMet() - else: - return False - - def removeMetadata( self, style ): - retcode = True - if style == MetaDataStyle.CIX: - retcode = self.removeCIX() - elif style == MetaDataStyle.CBI: - retcode = self.removeCBI() - elif style == MetaDataStyle.COMET: - retcode = self.removeCoMet() - return retcode - - def getPage( self, index ): - - image_data = None - - filename = self.getPageName( index ) - - if filename is not None: - try: - image_data = self.archiver.readArchiveFile( filename ) - except IOError: - print >> sys.stderr, u"Error reading in page. Substituting logo page." - image_data = ComicArchive.logo_data - - return image_data - - def getPageName( self, index ): - - if index is None: - return None - - page_list = self.getPageNameList() - - num_pages = len( page_list ) - if num_pages == 0 or index >= num_pages: - return None - - return page_list[index] - - def getScannerPageIndex( self ): - - scanner_page_index = None - - #make a guess at the scanner page - name_list = self.getPageNameList() - count = self.getNumberOfPages() - - #too few pages to really know - if count < 5: - return None - - # count the length of every filename, and count occurences - length_buckets = dict() - for name in name_list: - fname = os.path.split(name)[1] - length = len(fname) - if length_buckets.has_key( length ): - length_buckets[ length ] += 1 - else: - length_buckets[ length ] = 1 - - # sort by most common - sorted_buckets = sorted(length_buckets.iteritems(), key=lambda (k,v): (v,k), reverse=True) - - # statistical mode occurence is first - mode_length = sorted_buckets[0][0] - - # we are only going to consider the final image file: - final_name = os.path.split(name_list[count-1])[1] - - common_length_list = list() - for name in name_list: - if len(os.path.split(name)[1]) == mode_length: - common_length_list.append( os.path.split(name)[1] ) - - prefix = os.path.commonprefix(common_length_list) - - if mode_length <= 7 and prefix == "": - #probably all numbers - if len(final_name) > mode_length: - scanner_page_index = count-1 - - # see if the last page doesn't start with the same prefix as most others - elif not final_name.startswith(prefix): - scanner_page_index = count-1 - - return scanner_page_index - - - def getPageNameList( self , sort_list=True): - - if self.page_list is None: - # get the list file names in the archive, and sort - files = self.archiver.getArchiveFilenameList() - - # seems like some archive creators are on Windows, and don't know about case-sensitivity! - if sort_list: - def keyfunc(k): - #hack to account for some weird scanner ID pages - #basename=os.path.split(k)[1] - #if basename < '0': - # k = os.path.join(os.path.split(k)[0], "z" + basename) - return k.lower() - - files = natsorted(files, key=keyfunc,signed=False) - - # make a sub-list of image files - self.page_list = [] - for name in files: - if ( name[-4:].lower() in [ ".jpg", "jpeg", ".png", ".gif", "webp" ] and os.path.basename(name)[0] != "." ): - self.page_list.append(name) - - return self.page_list - - def getNumberOfPages( self ): - - if self.page_count is None: - self.page_count = len( self.getPageNameList( ) ) - return self.page_count - - def readCBI( self ): - if self.cbi_md is None: - raw_cbi = self.readRawCBI() - if raw_cbi is None: - self.cbi_md = GenericMetadata() - else: - self.cbi_md = ComicBookInfo().metadataFromString( raw_cbi ) - - self.cbi_md.setDefaultPageList( self.getNumberOfPages() ) - - return self.cbi_md - - def readRawCBI( self ): - if ( not self.hasCBI() ): - return None - - return self.archiver.getArchiveComment() - - def hasCBI(self): - if self.has_cbi is None: - - #if ( not ( self.isZip() or self.isRar()) or not self.seemsToBeAComicArchive() ): - if not self.seemsToBeAComicArchive(): - self.has_cbi = False - else: - comment = self.archiver.getArchiveComment() - self.has_cbi = ComicBookInfo().validateString( comment ) - - return self.has_cbi - - def writeCBI( self, metadata ): - if metadata is not None: - self.applyArchiveInfoToMetadata( metadata ) - cbi_string = ComicBookInfo().stringFromMetadata( metadata ) - write_success = self.archiver.setArchiveComment( cbi_string ) - if write_success: - self.has_cbi = True - self.cbi_md = metadata - self.resetCache() - return write_success - else: - return False - - def removeCBI( self ): - if self.hasCBI(): - write_success = self.archiver.setArchiveComment( "" ) - if write_success: - self.has_cbi = False - self.cbi_md = None - self.resetCache() - return write_success - return True - - def readCIX( self ): - if self.cix_md is None: - raw_cix = self.readRawCIX() - if raw_cix is None or raw_cix == "": - self.cix_md = GenericMetadata() - else: - self.cix_md = ComicInfoXml().metadataFromString( raw_cix ) - - #validate the existing page list (make sure count is correct) - if len ( self.cix_md.pages ) != 0 : - if len ( self.cix_md.pages ) != self.getNumberOfPages(): - # pages array doesn't match the actual number of images we're seeing - # in the archive, so discard the data - self.cix_md.pages = [] - - if len( self.cix_md.pages ) == 0: - self.cix_md.setDefaultPageList( self.getNumberOfPages() ) - - return self.cix_md - - def readRawCIX( self ): - if not self.hasCIX(): - return None - try: - raw_cix = self.archiver.readArchiveFile( self.ci_xml_filename ) - except IOError: - print "Error reading in raw CIX!" - raw_cix = "" - return raw_cix - - def writeCIX(self, metadata): - - if metadata is not None: - self.applyArchiveInfoToMetadata( metadata, calc_page_sizes=True ) - cix_string = ComicInfoXml().stringFromMetadata( metadata ) - write_success = self.archiver.writeArchiveFile( self.ci_xml_filename, cix_string ) - if write_success: - self.has_cix = True - self.cix_md = metadata - self.resetCache() - return write_success - else: - return False - - def removeCIX( self ): - if self.hasCIX(): - write_success = self.archiver.removeArchiveFile( self.ci_xml_filename ) - if write_success: - self.has_cix = False - self.cix_md = None - self.resetCache() - return write_success - return True - - - def hasCIX(self): - if self.has_cix is None: - - if not self.seemsToBeAComicArchive(): - self.has_cix = False - elif self.ci_xml_filename in self.archiver.getArchiveFilenameList(): - self.has_cix = True - else: - self.has_cix = False - return self.has_cix + def rarTest( self ): + try: + rarc = rarfile.RarFile( self.path ) + except: # InvalidRARArchive: + return False + else: + return True - def readCoMet( self ): - if self.comet_md is None: - raw_comet = self.readRawCoMet() - if raw_comet is None or raw_comet == "": - self.comet_md = GenericMetadata() - else: - self.comet_md = CoMet().metadataFromString( raw_comet ) - - self.comet_md.setDefaultPageList( self.getNumberOfPages() ) - #use the coverImage value from the comet_data to mark the cover in this struct - # walk through list of images in file, and find the matching one for md.coverImage - # need to remove the existing one in the default - if self.comet_md.coverImage is not None: - cover_idx = 0 - for idx,f in enumerate(self.getPageNameList()): - if self.comet_md.coverImage == f: - cover_idx = idx - break - if cover_idx != 0: - del (self.comet_md.pages[0]['Type'] ) - self.comet_md.pages[ cover_idx ]['Type'] = PageType.FrontCover + def isZip( self ): + return self.archive_type == self.ArchiveType.Zip - return self.comet_md + def isRar( self ): + return self.archive_type == self.ArchiveType.Rar + def isPdf(self): + return self.archive_type == self.ArchiveType.Pdf + def isFolder( self ): + return self.archive_type == self.ArchiveType.Folder - def readRawCoMet( self ): - if not self.hasCoMet(): - print >> sys.stderr, self.path, "doesn't have CoMet data!" - return None + def isWritable( self, check_rar_status=True ): + if self.archive_type == self.ArchiveType.Unknown : + return False - try: - raw_comet = self.archiver.readArchiveFile( self.comet_filename ) - except IOError: - print >> sys.stderr, u"Error reading in raw CoMet!" - raw_comet = "" - return raw_comet - - def writeCoMet(self, metadata): + elif check_rar_status and self.isRar() and self.rar_exe_path is None: + return False - if metadata is not None: - if not self.hasCoMet(): - self.comet_filename = self.comet_default_filename - - self.applyArchiveInfoToMetadata( metadata ) - # Set the coverImage value, if it's not the first page - cover_idx = int(metadata.getCoverPageIndexList()[0]) - if cover_idx != 0: - metadata.coverImage = self.getPageName( cover_idx ) - - comet_string = CoMet().stringFromMetadata( metadata ) - write_success = self.archiver.writeArchiveFile( self.comet_filename, comet_string ) - if write_success: - self.has_comet = True - self.comet_md = metadata - self.resetCache() - return write_success - else: - return False - - def removeCoMet( self ): - if self.hasCoMet(): - write_success = self.archiver.removeArchiveFile( self.comet_filename ) - if write_success: - self.has_comet = False - self.comet_md = None - self.resetCache() - return write_success - return True - - def hasCoMet(self): - if self.has_comet is None: - self.has_comet = False - if not self.seemsToBeAComicArchive(): - return self.has_comet - - #look at all xml files in root, and search for CoMet data, get first - for n in self.archiver.getArchiveFilenameList(): - if ( os.path.dirname(n) == "" and - os.path.splitext(n)[1].lower() == '.xml'): - # read in XML file, and validate it - try: - data = self.archiver.readArchiveFile( n ) - except: - data = "" - print >> sys.stderr, u"Error reading in Comet XML for validation!" - if CoMet().validateString( data ): - # since we found it, save it! - self.comet_filename = n - self.has_comet = True - break - - return self.has_comet - + elif not os.access(self.path, os.W_OK): + return False + + elif ((self.archive_type != self.ArchiveType.Folder) and + (not os.access( os.path.dirname( os.path.abspath(self.path)), os.W_OK ))): + return False + + return True + + def isWritableForStyle( self, data_style ): + + if self.isRar() and data_style == MetaDataStyle.CBI: + return False + + return self.isWritable() + + def seemsToBeAComicArchive( self ): + + # Do we even care about extensions?? + ext = os.path.splitext(self.path)[1].lower() + + if ( + ( self.isZip() or self.isRar() or self.isPdf()) #or self.isFolder() ) + and + ( self.getNumberOfPages() > 0) + + ): + return True + else: + return False + + def readMetadata( self, style ): + + if style == MetaDataStyle.CIX: + return self.readCIX() + elif style == MetaDataStyle.CBI: + return self.readCBI() + elif style == MetaDataStyle.COMET: + return self.readCoMet() + else: + return GenericMetadata() + + def writeMetadata( self, metadata, style ): + + retcode = None + if style == MetaDataStyle.CIX: + retcode = self.writeCIX( metadata ) + elif style == MetaDataStyle.CBI: + retcode = self.writeCBI( metadata ) + elif style == MetaDataStyle.COMET: + retcode = self.writeCoMet( metadata ) + return retcode - def applyArchiveInfoToMetadata( self, md, calc_page_sizes=False): - md.pageCount = self.getNumberOfPages() - - if calc_page_sizes: - for p in md.pages: - idx = int( p['Image'] ) - if pil_available: - if 'ImageSize' not in p or 'ImageHeight' not in p or 'ImageWidth' not in p: - data = self.getPage( idx ) - if data is not None: - try: - im = Image.open(StringIO.StringIO(data)) - w,h = im.size - - p['ImageSize'] = str(len(data)) - p['ImageHeight'] = str(h) - p['ImageWidth'] = str(w) - except IOError: - p['ImageSize'] = str(len(data)) - - else: - if 'ImageSize' not in p: - data = self.getPage( idx ) - p['ImageSize'] = str(len(data)) + def hasMetadata( self, style ): + + if style == MetaDataStyle.CIX: + return self.hasCIX() + elif style == MetaDataStyle.CBI: + return self.hasCBI() + elif style == MetaDataStyle.COMET: + return self.hasCoMet() + else: + return False + + def removeMetadata( self, style ): + retcode = True + if style == MetaDataStyle.CIX: + retcode = self.removeCIX() + elif style == MetaDataStyle.CBI: + retcode = self.removeCBI() + elif style == MetaDataStyle.COMET: + retcode = self.removeCoMet() + return retcode + + def getPage( self, index ): + + image_data = None + + filename = self.getPageName( index ) + + if filename is not None: + try: + image_data = self.archiver.readArchiveFile( filename ) + except IOError: + print >> sys.stderr, u"Error reading in page. Substituting logo page." + image_data = ComicArchive.logo_data + + return image_data + + def getPageName( self, index ): + + if index is None: + return None + + page_list = self.getPageNameList() + + num_pages = len( page_list ) + if num_pages == 0 or index >= num_pages: + return None + + return page_list[index] + + def getScannerPageIndex( self ): + + scanner_page_index = None + + #make a guess at the scanner page + name_list = self.getPageNameList() + count = self.getNumberOfPages() + + #too few pages to really know + if count < 5: + return None + + # count the length of every filename, and count occurences + length_buckets = dict() + for name in name_list: + fname = os.path.split(name)[1] + length = len(fname) + if length_buckets.has_key( length ): + length_buckets[ length ] += 1 + else: + length_buckets[ length ] = 1 + + # sort by most common + sorted_buckets = sorted(length_buckets.iteritems(), key=lambda (k,v): (v,k), reverse=True) + + # statistical mode occurence is first + mode_length = sorted_buckets[0][0] + + # we are only going to consider the final image file: + final_name = os.path.split(name_list[count-1])[1] + + common_length_list = list() + for name in name_list: + if len(os.path.split(name)[1]) == mode_length: + common_length_list.append( os.path.split(name)[1] ) + + prefix = os.path.commonprefix(common_length_list) + + if mode_length <= 7 and prefix == "": + #probably all numbers + if len(final_name) > mode_length: + scanner_page_index = count-1 + + # see if the last page doesn't start with the same prefix as most others + elif not final_name.startswith(prefix): + scanner_page_index = count-1 + + return scanner_page_index - - def metadataFromFilename( self , parse_scan_info=True): - - metadata = GenericMetadata() - - fnp = FileNameParser() - fnp.parseFilename( self.path ) + def getPageNameList( self , sort_list=True): - if fnp.issue != "": - metadata.issue = fnp.issue - if fnp.series != "": - metadata.series = fnp.series - if fnp.volume != "": - metadata.volume = fnp.volume - if fnp.year != "": - metadata.year = fnp.year - if fnp.issue_count != "": - metadata.issueCount = fnp.issue_count - if parse_scan_info: - if fnp.remainder != "": - metadata.scanInfo = fnp.remainder + if self.page_list is None: + # get the list file names in the archive, and sort + files = self.archiver.getArchiveFilenameList() - metadata.isEmpty = False + # seems like some archive creators are on Windows, and don't know about case-sensitivity! + if sort_list: + def keyfunc(k): + #hack to account for some weird scanner ID pages + #basename=os.path.split(k)[1] + #if basename < '0': + # k = os.path.join(os.path.split(k)[0], "z" + basename) + return k.lower() - return metadata + files = natsorted(files, key=keyfunc,signed=False) + + # make a sub-list of image files + self.page_list = [] + for name in files: + if ( name[-4:].lower() in [ ".jpg", "jpeg", ".png", ".gif", "webp" ] and os.path.basename(name)[0] != "." ): + self.page_list.append(name) + + return self.page_list + + def getNumberOfPages( self ): + + if self.page_count is None: + self.page_count = len( self.getPageNameList( ) ) + return self.page_count + + def readCBI( self ): + if self.cbi_md is None: + raw_cbi = self.readRawCBI() + if raw_cbi is None: + self.cbi_md = GenericMetadata() + else: + self.cbi_md = ComicBookInfo().metadataFromString( raw_cbi ) + + self.cbi_md.setDefaultPageList( self.getNumberOfPages() ) + + return self.cbi_md + + def readRawCBI( self ): + if ( not self.hasCBI() ): + return None + + return self.archiver.getArchiveComment() + + def hasCBI(self): + if self.has_cbi is None: + + #if ( not ( self.isZip() or self.isRar()) or not self.seemsToBeAComicArchive() ): + if not self.seemsToBeAComicArchive(): + self.has_cbi = False + else: + comment = self.archiver.getArchiveComment() + self.has_cbi = ComicBookInfo().validateString( comment ) + + return self.has_cbi + + def writeCBI( self, metadata ): + if metadata is not None: + self.applyArchiveInfoToMetadata( metadata ) + cbi_string = ComicBookInfo().stringFromMetadata( metadata ) + write_success = self.archiver.setArchiveComment( cbi_string ) + if write_success: + self.has_cbi = True + self.cbi_md = metadata + self.resetCache() + return write_success + else: + return False + + def removeCBI( self ): + if self.hasCBI(): + write_success = self.archiver.setArchiveComment( "" ) + if write_success: + self.has_cbi = False + self.cbi_md = None + self.resetCache() + return write_success + return True + + def readCIX( self ): + if self.cix_md is None: + raw_cix = self.readRawCIX() + if raw_cix is None or raw_cix == "": + self.cix_md = GenericMetadata() + else: + self.cix_md = ComicInfoXml().metadataFromString( raw_cix ) + + #validate the existing page list (make sure count is correct) + if len ( self.cix_md.pages ) != 0 : + if len ( self.cix_md.pages ) != self.getNumberOfPages(): + # pages array doesn't match the actual number of images we're seeing + # in the archive, so discard the data + self.cix_md.pages = [] + + if len( self.cix_md.pages ) == 0: + self.cix_md.setDefaultPageList( self.getNumberOfPages() ) + + return self.cix_md + + def readRawCIX( self ): + if not self.hasCIX(): + return None + try: + raw_cix = self.archiver.readArchiveFile( self.ci_xml_filename ) + except IOError: + print "Error reading in raw CIX!" + raw_cix = "" + return raw_cix + + def writeCIX(self, metadata): + + if metadata is not None: + self.applyArchiveInfoToMetadata( metadata, calc_page_sizes=True ) + cix_string = ComicInfoXml().stringFromMetadata( metadata ) + write_success = self.archiver.writeArchiveFile( self.ci_xml_filename, cix_string ) + if write_success: + self.has_cix = True + self.cix_md = metadata + self.resetCache() + return write_success + else: + return False + + def removeCIX( self ): + if self.hasCIX(): + write_success = self.archiver.removeArchiveFile( self.ci_xml_filename ) + if write_success: + self.has_cix = False + self.cix_md = None + self.resetCache() + return write_success + return True + + + def hasCIX(self): + if self.has_cix is None: + + if not self.seemsToBeAComicArchive(): + self.has_cix = False + elif self.ci_xml_filename in self.archiver.getArchiveFilenameList(): + self.has_cix = True + else: + self.has_cix = False + return self.has_cix + + + def readCoMet( self ): + if self.comet_md is None: + raw_comet = self.readRawCoMet() + if raw_comet is None or raw_comet == "": + self.comet_md = GenericMetadata() + else: + self.comet_md = CoMet().metadataFromString( raw_comet ) + + self.comet_md.setDefaultPageList( self.getNumberOfPages() ) + #use the coverImage value from the comet_data to mark the cover in this struct + # walk through list of images in file, and find the matching one for md.coverImage + # need to remove the existing one in the default + if self.comet_md.coverImage is not None: + cover_idx = 0 + for idx,f in enumerate(self.getPageNameList()): + if self.comet_md.coverImage == f: + cover_idx = idx + break + if cover_idx != 0: + del (self.comet_md.pages[0]['Type'] ) + self.comet_md.pages[ cover_idx ]['Type'] = PageType.FrontCover + + return self.comet_md + + def readRawCoMet( self ): + if not self.hasCoMet(): + print >> sys.stderr, self.path, "doesn't have CoMet data!" + return None + + try: + raw_comet = self.archiver.readArchiveFile( self.comet_filename ) + except IOError: + print >> sys.stderr, u"Error reading in raw CoMet!" + raw_comet = "" + return raw_comet + + def writeCoMet(self, metadata): + + if metadata is not None: + if not self.hasCoMet(): + self.comet_filename = self.comet_default_filename + + self.applyArchiveInfoToMetadata( metadata ) + # Set the coverImage value, if it's not the first page + cover_idx = int(metadata.getCoverPageIndexList()[0]) + if cover_idx != 0: + metadata.coverImage = self.getPageName( cover_idx ) + + comet_string = CoMet().stringFromMetadata( metadata ) + write_success = self.archiver.writeArchiveFile( self.comet_filename, comet_string ) + if write_success: + self.has_comet = True + self.comet_md = metadata + self.resetCache() + return write_success + else: + return False + + def removeCoMet( self ): + if self.hasCoMet(): + write_success = self.archiver.removeArchiveFile( self.comet_filename ) + if write_success: + self.has_comet = False + self.comet_md = None + self.resetCache() + return write_success + return True + + def hasCoMet(self): + if self.has_comet is None: + self.has_comet = False + if not self.seemsToBeAComicArchive(): + return self.has_comet + + #look at all xml files in root, and search for CoMet data, get first + for n in self.archiver.getArchiveFilenameList(): + if ( os.path.dirname(n) == "" and + os.path.splitext(n)[1].lower() == '.xml'): + # read in XML file, and validate it + try: + data = self.archiver.readArchiveFile( n ) + except: + data = "" + print >> sys.stderr, u"Error reading in Comet XML for validation!" + if CoMet().validateString( data ): + # since we found it, save it! + self.comet_filename = n + self.has_comet = True + break + + return self.has_comet + + + + def applyArchiveInfoToMetadata( self, md, calc_page_sizes=False): + md.pageCount = self.getNumberOfPages() + + if calc_page_sizes: + for p in md.pages: + idx = int( p['Image'] ) + if pil_available: + if 'ImageSize' not in p or 'ImageHeight' not in p or 'ImageWidth' not in p: + data = self.getPage( idx ) + if data is not None: + try: + im = Image.open(StringIO.StringIO(data)) + w,h = im.size + + p['ImageSize'] = str(len(data)) + p['ImageHeight'] = str(h) + p['ImageWidth'] = str(w) + except IOError: + p['ImageSize'] = str(len(data)) + + else: + if 'ImageSize' not in p: + data = self.getPage( idx ) + p['ImageSize'] = str(len(data)) + + + + def metadataFromFilename( self , parse_scan_info=True): + + metadata = GenericMetadata() + + fnp = FileNameParser() + fnp.parseFilename( self.path ) + + if fnp.issue != "": + metadata.issue = fnp.issue + if fnp.series != "": + metadata.series = fnp.series + if fnp.volume != "": + metadata.volume = fnp.volume + if fnp.year != "": + metadata.year = fnp.year + if fnp.issue_count != "": + metadata.issueCount = fnp.issue_count + if parse_scan_info: + if fnp.remainder != "": + metadata.scanInfo = fnp.remainder + + metadata.isEmpty = False + + return metadata + + def exportAsZip( self, zipfilename ): + if self.archive_type == self.ArchiveType.Zip: + # nothing to do, we're already a zip + return True + + zip_archiver = ZipArchiver( zipfilename ) + return zip_archiver.copyFromArchive( self.archiver ) - def exportAsZip( self, zipfilename ): - if self.archive_type == self.ArchiveType.Zip: - # nothing to do, we're already a zip - return True - - zip_archiver = ZipArchiver( zipfilename ) - return zip_archiver.copyFromArchive( self.archiver ) - From 18c4d19a0c1172191cc585374bb4c5e8bfc45d84 Mon Sep 17 00:00:00 2001 From: kounch Date: Sat, 2 Mar 2019 06:54:00 +0100 Subject: [PATCH 03/22] Python 3 basic compatibility --- comet.py | 6 +++--- comicarchive.py | 24 ++++++++++++------------ comicbookinfo.py | 12 ++++++------ comicinfoxml.py | 18 +++++++++--------- filenameparser.py | 2 +- genericmetadata.py | 4 ++-- issuestring.py | 4 +--- utils.py | 8 ++++---- 8 files changed, 38 insertions(+), 40 deletions(-) diff --git a/comet.py b/comet.py index 1a06977..dca1f31 100644 --- a/comet.py +++ b/comet.py @@ -22,8 +22,8 @@ from datetime import datetime import zipfile from pprint import pprint import xml.etree.ElementTree as ET -from genericmetadata import GenericMetadata -import utils +from comicapi.genericmetadata import GenericMetadata +import comicapi.utils class CoMet: @@ -213,7 +213,7 @@ class CoMet: for n in root: if n.tag == 'character': char_list.append(n.text.strip()) - md.characters = utils.listToString( char_list ) + md.characters = comicapi.utils.listToString( char_list ) # Now extract the credit info for n in root: diff --git a/comicarchive.py b/comicarchive.py index a2ef85c..1fad324 100644 --- a/comicarchive.py +++ b/comicarchive.py @@ -40,7 +40,6 @@ class OpenableRarFile(rarfile.RarFile): def open(self, member): #print "opening %s..." % member # based on https://github.com/matiasb/python-unrar/pull/4/files - res = [] if isinstance(member, rarfile.RarInfo): member = member.filename archive = unrarlib.RAROpenArchiveDataEx(self.filename, mode=constants.RAR_OM_EXTRACT) @@ -76,9 +75,10 @@ if platform.system() == "Windows": import _subprocess import time -import StringIO +from io import StringIO +from io import BytesIO try: - import Image + from PIL import Image pil_available = True except ImportError: pil_available = False @@ -88,11 +88,11 @@ sys.path.insert(0, os.path.abspath(".") ) #from UnRAR2.rar_exceptions import * #from settings import ComicTaggerSettings -from comicinfoxml import ComicInfoXml -from comicbookinfo import ComicBookInfo -from comet import CoMet -from genericmetadata import GenericMetadata, PageType -from filenameparser import FileNameParser +from comicapi.comicinfoxml import ComicInfoXml +from comicapi.comicbookinfo import ComicBookInfo +from comicapi.comet import CoMet +from comicapi.genericmetadata import GenericMetadata, PageType +from comicapi.filenameparser import FileNameParser from PyPDF2 import PdfFileReader class MetaDataStyle: @@ -803,13 +803,13 @@ class ComicArchive: for name in name_list: fname = os.path.split(name)[1] length = len(fname) - if length_buckets.has_key( length ): + if length in length_buckets: length_buckets[ length ] += 1 else: length_buckets[ length ] = 1 # sort by most common - sorted_buckets = sorted(length_buckets.iteritems(), key=lambda (k,v): (v,k), reverse=True) + sorted_buckets = sorted(length_buckets.items(), key=lambda k,v: (v,k), reverse=True) # statistical mode occurence is first mode_length = sorted_buckets[0][0] @@ -946,7 +946,7 @@ class ComicArchive: try: raw_cix = self.archiver.readArchiveFile( self.ci_xml_filename ) except IOError: - print "Error reading in raw CIX!" + print ("Error reading in raw CIX!") raw_cix = "" return raw_cix @@ -1092,7 +1092,7 @@ class ComicArchive: data = self.getPage( idx ) if data is not None: try: - im = Image.open(StringIO.StringIO(data)) + im = Image.open(StringIO(data)) w,h = im.size p['ImageSize'] = str(len(data)) diff --git a/comicbookinfo.py b/comicbookinfo.py index a0bbaf0..38b025c 100644 --- a/comicbookinfo.py +++ b/comicbookinfo.py @@ -23,8 +23,8 @@ import json from datetime import datetime import zipfile -from genericmetadata import GenericMetadata -import utils +from comicapi.genericmetadata import GenericMetadata +import comicapi.utils #import ctversion class ComicBookInfo: @@ -74,8 +74,8 @@ class ComicBookInfo: # reverse look-up pattern = metadata.language metadata.language = None - for key in utils.getLanguageDict(): - if utils.getLanguageDict()[ key ] == pattern.encode('utf-8'): + for key in comicapi.utils.getLanguageDict(): + if comicapi.utils.getLanguageDict()[ key ] == pattern.encode('utf-8'): metadata.language = key break @@ -115,7 +115,7 @@ class ComicBookInfo: #helper func def toInt(s): i = None - if type(s) in [ str, unicode, int ]: + if type(s) in [ str, int ]: try: i = int(s) except ValueError: @@ -133,7 +133,7 @@ class ComicBookInfo: assign( 'genre', metadata.genre ) assign( 'volume', toInt(metadata.volume) ) assign( 'numberOfVolumes', toInt(metadata.volumeCount) ) - assign( 'language', utils.getLanguageFromISO(metadata.language) ) + assign( 'language', comicapi.utils.getLanguageFromISO(metadata.language) ) assign( 'country', metadata.country ) assign( 'rating', metadata.criticalRating ) assign( 'credits', metadata.credits ) diff --git a/comicinfoxml.py b/comicinfoxml.py index 9e9df07..6301782 100644 --- a/comicinfoxml.py +++ b/comicinfoxml.py @@ -22,8 +22,8 @@ from datetime import datetime import zipfile from pprint import pprint import xml.etree.ElementTree as ET -from genericmetadata import GenericMetadata -import utils +from comicapi.genericmetadata import GenericMetadata +import comicapi.utils class ComicInfoXml: @@ -141,31 +141,31 @@ class ComicInfoXml: # second, convert each list to string, and add to XML struct if len( credit_writer_list ) > 0: node = ET.SubElement(root, 'Writer') - node.text = utils.listToString( credit_writer_list ) + node.text = comicapi.utils.listToString( credit_writer_list ) if len( credit_penciller_list ) > 0: node = ET.SubElement(root, 'Penciller') - node.text = utils.listToString( credit_penciller_list ) + node.text = comicapi.utils.listToString( credit_penciller_list ) if len( credit_inker_list ) > 0: node = ET.SubElement(root, 'Inker') - node.text = utils.listToString( credit_inker_list ) + node.text = comicapi.utils.listToString( credit_inker_list ) if len( credit_colorist_list ) > 0: node = ET.SubElement(root, 'Colorist') - node.text = utils.listToString( credit_colorist_list ) + node.text = comicapi.utils.listToString( credit_colorist_list ) if len( credit_letterer_list ) > 0: node = ET.SubElement(root, 'Letterer') - node.text = utils.listToString( credit_letterer_list ) + node.text = comicapi.utils.listToString( credit_letterer_list ) if len( credit_cover_list ) > 0: node = ET.SubElement(root, 'CoverArtist') - node.text = utils.listToString( credit_cover_list ) + node.text = comicapi.utils.listToString( credit_cover_list ) if len( credit_editor_list ) > 0: node = ET.SubElement(root, 'Editor') - node.text = utils.listToString( credit_editor_list ) + node.text = comicapi.utils.listToString( credit_editor_list ) assign( 'Publisher', md.publisher ) assign( 'Imprint', md.imprint ) diff --git a/filenameparser.py b/filenameparser.py index 6f3aa05..052a99b 100644 --- a/filenameparser.py +++ b/filenameparser.py @@ -27,7 +27,7 @@ limitations under the License. import re import os -from urllib import unquote +from urllib.parse import unquote class FileNameParser: diff --git a/genericmetadata.py b/genericmetadata.py index 8e7aeaf..2fe4fe5 100644 --- a/genericmetadata.py +++ b/genericmetadata.py @@ -23,7 +23,7 @@ See the License for the specific language governing permissions and limitations under the License. """ -import utils +import comicapi.utils # These page info classes are exactly the same as the CIX scheme, since it's unique class PageType: @@ -293,7 +293,7 @@ class GenericMetadata: add_attr_string( "comments" ) add_attr_string( "notes" ) - add_string( "tags", utils.listToString( self.tags ) ) + add_string( "tags", comicapi.utils.listToString( self.tags ) ) for c in self.credits: primary = "" diff --git a/issuestring.py b/issuestring.py index 751aa8c..62c0b0a 100644 --- a/issuestring.py +++ b/issuestring.py @@ -29,7 +29,7 @@ See the License for the specific language governing permissions and limitations under the License. """ -import utils +import comicapi.utils import math import re @@ -51,8 +51,6 @@ class IssueString: if len(text) == 0: return - text = unicode(text) - #skip the minus sign if it's first if text[0] == '-': start = 1 diff --git a/utils.py b/utils.py index e315cd7..ded8efa 100644 --- a/utils.py +++ b/utils.py @@ -59,9 +59,9 @@ def get_recursive_filelist( pathlist ): if type(p) == str: #make sure string is unicode p = p.decode(filename_encoding) #, 'replace') - elif type(p) != unicode: + elif type(p) != str: #it's probably a QString - p = unicode(p) + p = str(p) if os.path.isdir( p ): for root,dirs,files in os.walk( p ): @@ -69,9 +69,9 @@ def get_recursive_filelist( pathlist ): if type(f) == str: #make sure string is unicode f = f.decode(filename_encoding, 'replace') - elif type(f) != unicode: + elif type(f) != str: #it's probably a QString - f = unicode(f) + f = str(f) filelist.append(os.path.join(root,f)) else: filelist.append(p) From 63932f9bf8b460f3b5654d67ce75fb0c932baaed Mon Sep 17 00:00:00 2001 From: kounch Date: Sun, 3 Mar 2019 10:28:21 +0100 Subject: [PATCH 04/22] Ported to Python3 --- comicarchive.py | 46 ++++++++++++++++++++++++++++++---------------- 1 file changed, 30 insertions(+), 16 deletions(-) diff --git a/comicarchive.py b/comicarchive.py index 1fad324..7337bc0 100644 --- a/comicarchive.py +++ b/comicarchive.py @@ -68,7 +68,7 @@ class OpenableRarFile(rarfile.RarFile): self._close(handle) if not found: raise KeyError('There is no item named %r in the archive' % member) - return ''.join(buf) + return b''.join(buf) if platform.system() == "Windows": @@ -122,12 +122,14 @@ class ZipArchiver: try: data = zf.read( archive_file ) except zipfile.BadZipfile as e: - print >> sys.stderr, u"bad zipfile [{0}]: {1} :: {2}".format(e, self.path, archive_file) + errMsg=u"bad zipfile [{0}]: {1} :: {2}".format(e, self.path, archive_file) + sys.stderr.buffer.write(bytes(errMsg,"UTF-8")) zf.close() raise IOError except Exception as e: zf.close() - print >> sys.stderr, u"bad zipfile [{0}]: {1} :: {2}".format(e, self.path, archive_file) + errMsg=u"bad zipfile [{0}]: {1} :: {2}".format(e, self.path, archive_file) + sys.stderr.buffer.write(bytes(errMsg,"UTF-8")) raise IOError finally: zf.close() @@ -163,14 +165,15 @@ class ZipArchiver: zf.close() return namelist except Exception as e: - print >> sys.stderr, u"Unable to get zipfile list [{0}]: {1}".format(e, self.path) + errMsg=u"Unable to get zipfile list [{0}]: {1}".format(e, self.path) + sys.stderr.buffer.write(bytes(errMsg,"UTF-8")) return [] # zip helper func def rebuildZipFile( self, exclude_list ): # this recompresses the zip archive, without the files in the exclude_list - #print ">> sys.stderr, Rebuilding zip {0} without {1}".format( self.path, exclude_list ) + #errMsg=u"Rebuilding zip {0} without {1}".format( self.path, exclude_list ) # generate temp file tmp_fd, tmp_name = tempfile.mkstemp( dir=os.path.dirname(self.path) ) @@ -273,7 +276,8 @@ class ZipArchiver: if not self.writeZipComment( self.path, comment ): return False except Exception as e: - print >> sys.stderr, u"Error while copying to {0}: {1}".format(self.path, e) + errMsg=u"Error while copying to {0}: {1}".format(self.path, e) + sys.stderr.buffer.write(bytes(errMsg,"UTF-8")) return False else: return True @@ -362,22 +366,26 @@ class RarArchiver: #entries = rarc.read_files( archive_file ) if entries[0][0].file_size != len(entries[0][1]): - print >> sys.stderr, u"readArchiveFile(): [file is not expected size: {0} vs {1}] {2}:{3} [attempt # {4}]".format( + errMsg=u"readArchiveFile(): [file is not expected size: {0} vs {1}] {2}:{3} [attempt # {4}]".format( entries[0][0].file_size,len(entries[0][1]), self.path, archive_file, tries) + sys.stderr.buffer.write(bytes(errMsg,"UTF-8")) continue except (OSError, IOError) as e: - print >> sys.stderr, u"readArchiveFile(): [{0}] {1}:{2} attempt#{3}".format(str(e), self.path, archive_file, tries) + errMsg=u"readArchiveFile(): [{0}] {1}:{2} attempt#{3}".format(str(e), self.path, archive_file, tries) + sys.stderr.buffer.write(bytes(errMsg,"UTF-8")) time.sleep(1) except Exception as e: - print >> sys.stderr, u"Unexpected exception in readArchiveFile(): [{0}] for {1}:{2} attempt#{3}".format(str(e), self.path, archive_file, tries) + errMsg=u"Unexpected exception in readArchiveFile(): [{0}] for {1}:{2} attempt#{3}".format(str(e), self.path, archive_file, tries) + sys.stderr.buffer.write(bytes(errMsg,"UTF-8")) break else: #Success" #entries is a list of of tuples: ( rarinfo, filedata) if tries > 1: - print >> sys.stderr, u"Attempted read_files() {0} times".format(tries) + errMsg=u"Attempted read_files() {0} times".format(tries) + sys.stderr.buffer.write(bytes(errMsg,"UTF-8")) if (len(entries) == 1): return entries[0][1] else: @@ -453,7 +461,8 @@ class RarArchiver: namelist.append( item.filename ) except (OSError, IOError) as e: - print >> sys.stderr, u"getArchiveFilenameList(): [{0}] {1} attempt#{2}".format(str(e), self.path, tries) + errMsg=u"getArchiveFilenameList(): [{0}] {1} attempt#{2}".format(str(e), self.path, tries) + sys.stderr.buffer.write(bytes(errMsg,"UTF-8")) time.sleep(1) else: @@ -472,7 +481,8 @@ class RarArchiver: rarc = OpenableRarFile(self.path) except (OSError, IOError) as e: - print >> sys.stderr, u"getRARObj(): [{0}] {1} attempt#{2}".format(str(e), self.path, tries) + errMsg=u"getRARObj(): [{0}] {1} attempt#{2}".format(str(e), self.path, tries) + sys.stderr.buffer.write(bytes(errMsg,"UTF-8")) time.sleep(1) else: @@ -768,7 +778,8 @@ class ComicArchive: try: image_data = self.archiver.readArchiveFile( filename ) except IOError: - print >> sys.stderr, u"Error reading in page. Substituting logo page." + errMsg=u"Error reading in page. Substituting logo page." + sys.stderr.buffer.write(bytes(errMsg,"UTF-8")) image_data = ComicArchive.logo_data return image_data @@ -1013,13 +1024,15 @@ class ComicArchive: def readRawCoMet( self ): if not self.hasCoMet(): - print >> sys.stderr, self.path, "doesn't have CoMet data!" + errMsg=u"{} doesn't have CoMet data!".format(self.path) + sys.stderr.buffer.write(bytes(errMsg,"UTF-8")) return None try: raw_comet = self.archiver.readArchiveFile( self.comet_filename ) except IOError: - print >> sys.stderr, u"Error reading in raw CoMet!" + errMsg=u"Error reading in raw CoMet!" + sys.stderr.buffer.write(bytes(errMsg,"UTF-8")) raw_comet = "" return raw_comet @@ -1070,7 +1083,8 @@ class ComicArchive: data = self.archiver.readArchiveFile( n ) except: data = "" - print >> sys.stderr, u"Error reading in Comet XML for validation!" + errMsg=u"Error reading in Comet XML for validation!" + sys.stderr.buffer.write(bytes(errMsg,"UTF-8")) if CoMet().validateString( data ): # since we found it, save it! self.comet_filename = n From f893cadfbe75003b16541346efc259ae6780e40f Mon Sep 17 00:00:00 2001 From: kounch Date: Sun, 3 Mar 2019 20:27:46 +0100 Subject: [PATCH 05/22] More changes for Python 3 --- comicbookinfo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/comicbookinfo.py b/comicbookinfo.py index 38b025c..e1edaba 100644 --- a/comicbookinfo.py +++ b/comicbookinfo.py @@ -32,7 +32,7 @@ class ComicBookInfo: def metadataFromString( self, string ): - cbi_container = json.loads( unicode(string, 'utf-8') ) + cbi_container = json.loads( str(string, 'utf-8') ) metadata = GenericMetadata() From aa8197a8bb6ae338879b7c4b14b754af70378621 Mon Sep 17 00:00:00 2001 From: kounch Date: Mon, 4 Mar 2019 20:26:45 +0100 Subject: [PATCH 06/22] Windows compatibility and instructions for pybonjour on Python 3.7 --- comicarchive.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/comicarchive.py b/comicarchive.py index 7337bc0..51f15c7 100644 --- a/comicarchive.py +++ b/comicarchive.py @@ -71,8 +71,8 @@ class OpenableRarFile(rarfile.RarFile): return b''.join(buf) -if platform.system() == "Windows": - import _subprocess +# if platform.system() == "Windows": +# import _subprocess import time from io import StringIO @@ -298,8 +298,9 @@ class RarArchiver: # windows only, keeps the cmd.exe from popping up if platform.system() == "Windows": - self.startupinfo = subprocess.STARTUPINFO() - self.startupinfo.dwFlags |= _subprocess.STARTF_USESHOWWINDOW + # self.startupinfo = subprocess.STARTUPINFO() + # self.startupinfo.dwFlags |= _subprocess.STARTF_USESHOWWINDOW + self.startupinfo = None else: self.startupinfo = None From ab50622906889db5c8f1026338be26b2fbf2ef6c Mon Sep 17 00:00:00 2001 From: kounch Date: Wed, 6 Mar 2019 18:39:03 +0100 Subject: [PATCH 07/22] Revised format (PEP8) and more python3 porting --- UnRAR2/__init__.py | 40 +- UnRAR2/rar_exceptions.py | 23 +- UnRAR2/test_UnRAR2.py | 54 +- UnRAR2/unix.py | 98 ++-- UnRAR2/windows.py | 215 ++++---- comet.py | 410 ++++++++------- comicarchive.py | 602 ++++++++++++---------- comicbookinfo.py | 224 ++++---- comicinfoxml.py | 460 ++++++++--------- filenameparser.py | 443 ++++++++-------- genericmetadata.py | 521 ++++++++++--------- issuestring.py | 192 ++++--- utils.py | 1051 +++++++++++++++++++------------------- 13 files changed, 2194 insertions(+), 2139 deletions(-) mode change 100644 => 100755 UnRAR2/__init__.py mode change 100644 => 100755 UnRAR2/rar_exceptions.py mode change 100644 => 100755 UnRAR2/test_UnRAR2.py mode change 100644 => 100755 UnRAR2/unix.py mode change 100644 => 100755 UnRAR2/windows.py mode change 100644 => 100755 comet.py mode change 100644 => 100755 comicarchive.py mode change 100644 => 100755 comicbookinfo.py mode change 100644 => 100755 comicinfoxml.py mode change 100644 => 100755 filenameparser.py mode change 100644 => 100755 genericmetadata.py mode change 100644 => 100755 issuestring.py mode change 100644 => 100755 utils.py diff --git a/UnRAR2/__init__.py b/UnRAR2/__init__.py old mode 100644 new mode 100755 index a913fcb..3e043ce --- a/UnRAR2/__init__.py +++ b/UnRAR2/__init__.py @@ -19,7 +19,6 @@ # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. - """ pyUnRAR2 is a ctypes based wrapper around the free UnRAR.dll. @@ -42,13 +41,13 @@ except NameError: in_windows = False if in_windows: - from windows import RarFileImplementation + from comicapi.UnRAR2.windows import RarFileImplementation else: - from unix import RarFileImplementation - - + from comicapi.UnRAR2.unix import RarFileImplementation + import fnmatch, time, weakref + class RarInfo(object): """Represents a file header in an archive. Don't instantiate directly. Use only to obtain information about file. @@ -74,18 +73,16 @@ class RarInfo(object): self.size = data['size'] self.datetime = data['datetime'] self.comment = data['comment'] - - def __str__(self): - try : + try: arcName = self.rarfile.archiveName except ReferenceError: arcName = "[ARCHIVE_NO_LONGER_LOADED]" return '' % (self.filename, arcName) -class RarFile(RarFileImplementation): +class RarFile(RarFileImplementation): def __init__(self, archiveName, password=None): """Instantiate the archive. @@ -98,8 +95,7 @@ class RarFile(RarFileImplementation): >>> print RarFile('test.rar').comment This is a test. """ - self.archiveName = archiveName - RarFileImplementation.init(self, password) + RarFileImplementation.init(self, archiveName, password) def __del__(self): self.destruct() @@ -138,9 +134,12 @@ class RarFile(RarFileImplementation): """ checker = condition2checker(condition) return RarFileImplementation.read_files(self, checker) - - def extract(self, condition='*', path='.', withSubpath=True, overwrite=True): + def extract(self, + condition='*', + path='.', + withSubpath=True, + overwrite=True): """Extract specific files from archive to disk. If "condition" is a list of numbers, then extract files which have those positions in infolist. @@ -157,21 +156,26 @@ class RarFile(RarFileImplementation): Returns list of RarInfos for extracted files.""" checker = condition2checker(condition) - return RarFileImplementation.extract(self, checker, path, withSubpath, overwrite) + return RarFileImplementation.extract(self, checker, path, withSubpath, + overwrite) + def condition2checker(condition): """Converts different condition types to callback""" - if type(condition) in [str, unicode]: + if type(condition) in [str]: + def smatcher(info): return fnmatch.fnmatch(info.filename, condition) + return smatcher - elif type(condition) in [list, tuple] and type(condition[0]) in [int, long]: + elif type(condition) in [list, tuple] and type( + condition[0]) in [int]: + def imatcher(info): return info.index in condition + return imatcher elif callable(condition): return condition else: raise TypeError - - diff --git a/UnRAR2/rar_exceptions.py b/UnRAR2/rar_exceptions.py old mode 100644 new mode 100755 index d90d1c8..2cf94f7 --- a/UnRAR2/rar_exceptions.py +++ b/UnRAR2/rar_exceptions.py @@ -23,8 +23,21 @@ # Low level interface - see UnRARDLL\UNRARDLL.TXT -class ArchiveHeaderBroken(Exception): pass -class InvalidRARArchive(Exception): pass -class FileOpenError(Exception): pass -class IncorrectRARPassword(Exception): pass -class InvalidRARArchiveUsage(Exception): pass +class ArchiveHeaderBroken(Exception): + pass + + +class InvalidRARArchive(Exception): + pass + + +class FileOpenError(Exception): + pass + + +class IncorrectRARPassword(Exception): + pass + + +class InvalidRARArchiveUsage(Exception): + pass diff --git a/UnRAR2/test_UnRAR2.py b/UnRAR2/test_UnRAR2.py old mode 100644 new mode 100755 index e86ba2c..fcb2597 --- a/UnRAR2/test_UnRAR2.py +++ b/UnRAR2/test_UnRAR2.py @@ -1,7 +1,7 @@ import os, sys -import UnRAR2 -from UnRAR2.rar_exceptions import * +import comicapi.UnRAR2 as UnRAR2 +from comicapi.UnRAR2.rar_exceptions import * def cleanup(dir='test'): @@ -19,30 +19,30 @@ rarc.infolist() assert rarc.comment == "This is a test." for info in rarc.infoiter(): saveinfo = info - assert (str(info)=="""""") + assert (str(info) == """""") break rarc.extract() -assert os.path.exists('test'+os.sep+'test.txt') -assert os.path.exists('test'+os.sep+'this.py') +assert os.path.exists('test' + os.sep + 'test.txt') +assert os.path.exists('test' + os.sep + 'this.py') del rarc -assert (str(saveinfo)=="""""") +assert ( + str(saveinfo) == """""") cleanup() # extract all the files in test.rar cleanup() UnRAR2.RarFile('test.rar').extract() -assert os.path.exists('test'+os.sep+'test.txt') -assert os.path.exists('test'+os.sep+'this.py') +assert os.path.exists('test' + os.sep + 'test.txt') +assert os.path.exists('test' + os.sep + 'this.py') cleanup() # extract all the files in test.rar matching the wildcard *.txt cleanup() UnRAR2.RarFile('test.rar').extract('*.txt') -assert os.path.exists('test'+os.sep+'test.txt') -assert not os.path.exists('test'+os.sep+'this.py') +assert os.path.exists('test' + os.sep + 'test.txt') +assert not os.path.exists('test' + os.sep + 'this.py') cleanup() - # check the name and size of each file, extracting small ones cleanup() archive = UnRAR2.RarFile('test.rar') @@ -51,44 +51,42 @@ archive.extract(lambda rarinfo: rarinfo.size <= 1024) for rarinfo in archive.infoiter(): if rarinfo.size <= 1024 and not rarinfo.isdir: assert rarinfo.size == os.stat(rarinfo.filename).st_size -assert file('test'+os.sep+'test.txt', 'rt').read() == 'This is only a test.' -assert not os.path.exists('test'+os.sep+'this.py') +assert open('test' + os.sep + 'test.txt', + 'rt').read() == 'This is only a test.' +assert not os.path.exists('test' + os.sep + 'this.py') cleanup() - # extract this.py, overriding it's destination cleanup('test2') archive = UnRAR2.RarFile('test.rar') archive.extract('*.py', 'test2', False) -assert os.path.exists('test2'+os.sep+'this.py') +assert os.path.exists('test2' + os.sep + 'this.py') cleanup('test2') - # extract test.txt to memory cleanup() archive = UnRAR2.RarFile('test.rar') entries = UnRAR2.RarFile('test.rar').read_files('*test.txt') -assert len(entries)==1 +assert len(entries) == 1 assert entries[0][0].filename.endswith('test.txt') -assert entries[0][1]=='This is only a test.' - +assert entries[0][1] == 'This is only a test.' # extract all the files in test.rar with overwriting cleanup() -fo = open('test'+os.sep+'test.txt',"wt") +fo = open('test' + os.sep + 'test.txt', "wt") fo.write("blah") fo.close() UnRAR2.RarFile('test.rar').extract('*.txt') -assert open('test'+os.sep+'test.txt',"rt").read()!="blah" +assert open('test' + os.sep + 'test.txt', "rt").read() != "blah" cleanup() # extract all the files in test.rar without overwriting cleanup() -fo = open('test'+os.sep+'test.txt',"wt") +fo = open('test' + os.sep + 'test.txt', "wt") fo.write("blahblah") fo.close() -UnRAR2.RarFile('test.rar').extract('*.txt', overwrite = False) -assert open('test'+os.sep+'test.txt',"rt").read()=="blahblah" +UnRAR2.RarFile('test.rar').extract('*.txt', overwrite=False) +assert open('test' + os.sep + 'test.txt', "rt").read() == "blahblah" cleanup() # list big file in an archive @@ -98,28 +96,28 @@ list(UnRAR2.RarFile('test_nulls.rar').infoiter()) cleanup() rarc = UnRAR2.RarFile('test_protected_files.rar', password="protected") rarc.extract() -assert os.path.exists('test'+os.sep+'top_secret_xxx_file.txt') +assert os.path.exists('test' + os.sep + 'top_secret_xxx_file.txt') cleanup() errored = False try: UnRAR2.RarFile('test_protected_files.rar', password="proteqted").extract() except IncorrectRARPassword: errored = True -assert not os.path.exists('test'+os.sep+'top_secret_xxx_file.txt') +assert not os.path.exists('test' + os.sep + 'top_secret_xxx_file.txt') assert errored cleanup() # extract files from an archive with protected headers cleanup() UnRAR2.RarFile('test_protected_headers.rar', password="secret").extract() -assert os.path.exists('test'+os.sep+'top_secret_xxx_file.txt') +assert os.path.exists('test' + os.sep + 'top_secret_xxx_file.txt') cleanup() errored = False try: UnRAR2.RarFile('test_protected_headers.rar', password="seqret").extract() except IncorrectRARPassword: errored = True -assert not os.path.exists('test'+os.sep+'top_secret_xxx_file.txt') +assert not os.path.exists('test' + os.sep + 'top_secret_xxx_file.txt') assert errored cleanup() diff --git a/UnRAR2/unix.py b/UnRAR2/unix.py old mode 100644 new mode 100755 index bd9ee85..b962975 --- a/UnRAR2/unix.py +++ b/UnRAR2/unix.py @@ -28,13 +28,17 @@ import gc import os, os.path import time, re -from rar_exceptions import * +from comicapi.UnRAR2.rar_exceptions import * + + +class UnpackerNotInstalled(Exception): + pass -class UnpackerNotInstalled(Exception): pass rar_executable_cached = None rar_executable_version = None + def call_unrar(params): "Calls rar/unrar command line executable, returns stdout pipe" global rar_executable_cached @@ -48,29 +52,30 @@ def call_unrar(params): pass if rar_executable_cached is None: raise UnpackerNotInstalled("No suitable RAR unpacker installed") - + assert type(params) == list, "params must be list" args = [rar_executable_cached] + params try: - gc.disable() # See http://bugs.python.org/issue1336 - return subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + gc.disable() # See http://bugs.python.org/issue1336 + return subprocess.Popen( + args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) finally: gc.enable() -class RarFileImplementation(object): - def init(self, password=None): +class RarFileImplementation(object): + def init(self, archiveName='', password=None): global rar_executable_version + self.archiveName = archiveName self.password = password - - + stdoutdata, stderrdata = self.call('v', []).communicate() - + for line in stderrdata.splitlines(): if line.strip().startswith("Cannot open"): raise FileOpenError - if line.find("CRC failed")>=0: - raise IncorrectRARPassword + if line.find("CRC failed") >= 0: + raise IncorrectRARPassword accum = [] source = iter(stdoutdata.splitlines()) line = '' @@ -82,7 +87,8 @@ class RarFileImplementation(object): # but I see no safe way to rewrite it using a CLI tool if signature.startswith("UNRAR 4"): rar_executable_version = 4 - while not (line.startswith('Comment:') or line.startswith('Pathname/Comment')): + while not (line.startswith('Comment:') + or line.startswith('Pathname/Comment')): if line.strip().endswith('is not RAR archive'): raise InvalidRARArchive line = source.next() @@ -90,7 +96,7 @@ class RarFileImplementation(object): accum.append(line.rstrip('\n')) line = source.next() if len(accum): - accum[0] = accum[0][9:] # strip out "Comment:" part + accum[0] = accum[0][9:] # strip out "Comment:" part self.comment = '\n'.join(accum[:-1]) else: self.comment = None @@ -107,36 +113,36 @@ class RarFileImplementation(object): else: self.comment = None else: - raise UnpackerNotInstalled("Unsupported RAR version, expected 4.x or 5.x, found: " - + signature.split(" ")[1]) - - + raise UnpackerNotInstalled( + "Unsupported RAR version, expected 4.x or 5.x, found: " + + signature.split(" ")[1]) + def escaped_password(self): return '-' if self.password == None else self.password - - + def call(self, cmd, options=[], files=[]): - options2 = options + ['p'+self.escaped_password()] - soptions = ['-'+x for x in options2] - return call_unrar([cmd]+soptions+['--',self.archiveName]+files) + options2 = options + ['p' + self.escaped_password()] + soptions = ['-' + x for x in options2] + return call_unrar([cmd] + soptions + ['--', self.archiveName] + files) def infoiter(self): - + command = "v" if rar_executable_version == 4 else "l" stdoutdata, stderrdata = self.call(command, ['c-']).communicate() - + for line in stderrdata.splitlines(): if line.strip().startswith("Cannot open"): raise FileOpenError - + accum = [] source = iter(stdoutdata.splitlines()) line = '' while not line.startswith('-----------'): if line.strip().endswith('is not RAR archive'): raise InvalidRARArchive - if line.startswith("CRC failed") or line.startswith("Checksum error"): - raise IncorrectRARPassword + if line.startswith("CRC failed") or line.startswith( + "Checksum error"): + raise IncorrectRARPassword line = source.next() line = source.next() i = 0 @@ -144,16 +150,18 @@ class RarFileImplementation(object): if rar_executable_version == 4: while not line.startswith('-----------'): accum.append(line) - if len(accum)==2: + if len(accum) == 2: data = {} data['index'] = i # asterisks mark password-encrypted files - data['filename'] = accum[0].strip().lstrip("*") # asterisks marks password-encrypted files + data['filename'] = accum[0].strip().lstrip( + "*") # asterisks marks password-encrypted files fields = re_spaces.split(accum[1].strip()) data['size'] = int(fields[0]) attr = fields[5] data['isdir'] = 'd' in attr.lower() - data['datetime'] = time.strptime(fields[3]+" "+fields[4], '%d-%m-%y %H:%M') + data['datetime'] = time.strptime( + fields[3] + " " + fields[4], '%d-%m-%y %H:%M') data['comment'] = None yield data accum = [] @@ -168,23 +176,22 @@ class RarFileImplementation(object): data['size'] = int(fields[1]) attr = fields[0] data['isdir'] = 'd' in attr.lower() - data['datetime'] = time.strptime(fields[2]+" "+fields[3], '%d-%m-%y %H:%M') + data['datetime'] = time.strptime(fields[2] + " " + fields[3], + '%d-%m-%y %H:%M') data['comment'] = None yield data i += 1 line = source.next() - def read_files(self, checker): res = [] for info in self.infoiter(): checkres = checker(info) - if checkres==True and not info.isdir: + if checkres == True and not info.isdir: pipe = self.call('p', ['inul'], [info.filename]).stdout res.append((info, pipe.read())) - return res + return res - def extract(self, checker, path, withSubpath, overwrite): res = [] command = 'x' @@ -200,19 +207,20 @@ class RarFileImplementation(object): names = [] for info in self.infoiter(): checkres = checker(info) - if type(checkres) in [str, unicode]: - raise NotImplementedError("Condition callbacks returning strings are deprecated and only supported in Windows") - if checkres==True and not info.isdir: + if type(checkres) in [str]: + raise NotImplementedError( + "Condition callbacks returning strings are deprecated and only supported in Windows" + ) + if checkres == True and not info.isdir: names.append(info.filename) res.append(info) names.append(path) proc = self.call(command, options, names) stdoutdata, stderrdata = proc.communicate() - if stderrdata.find("CRC failed")>=0 or stderrdata.find("Checksum error")>=0: - raise IncorrectRARPassword - return res - + if stderrdata.find("CRC failed") >= 0 or stderrdata.find( + "Checksum error") >= 0: + raise IncorrectRARPassword + return res + def destruct(self): pass - - diff --git a/UnRAR2/windows.py b/UnRAR2/windows.py old mode 100644 new mode 100755 index bb92481..b1e71a5 --- a/UnRAR2/windows.py +++ b/UnRAR2/windows.py @@ -29,7 +29,7 @@ import os, os.path, sys import Queue import time -from rar_exceptions import * +from comicapi.UnRAR2.rar_exceptions import * ERAR_END_ARCHIVE = 10 ERAR_NO_MEMORY = 11 @@ -61,63 +61,74 @@ UCM_CHANGEVOLUME = 0 UCM_PROCESSDATA = 1 UCM_NEEDPASSWORD = 2 -architecture_bits = ctypes.sizeof(ctypes.c_voidp)*8 +architecture_bits = ctypes.sizeof(ctypes.c_voidp) * 8 dll_name = "unrar.dll" if architecture_bits == 64: dll_name = "x64\\unrar64.dll" - - + try: - unrar = ctypes.WinDLL(os.path.join(os.path.split(__file__)[0], 'UnRARDLL', dll_name)) + unrar = ctypes.WinDLL( + os.path.join(os.path.split(__file__)[0], 'UnRARDLL', dll_name)) except WindowsError: unrar = ctypes.WinDLL(dll_name) class RAROpenArchiveDataEx(ctypes.Structure): def __init__(self, ArcName=None, ArcNameW=u'', OpenMode=RAR_OM_LIST): - self.CmtBuf = ctypes.c_buffer(64*1024) - ctypes.Structure.__init__(self, ArcName=ArcName, ArcNameW=ArcNameW, OpenMode=OpenMode, _CmtBuf=ctypes.addressof(self.CmtBuf), CmtBufSize=ctypes.sizeof(self.CmtBuf)) + self.CmtBuf = ctypes.c_buffer(64 * 1024) + ctypes.Structure.__init__( + self, + ArcName=ArcName, + ArcNameW=ArcNameW, + OpenMode=OpenMode, + _CmtBuf=ctypes.addressof(self.CmtBuf), + CmtBufSize=ctypes.sizeof(self.CmtBuf)) _fields_ = [ - ('ArcName', ctypes.c_char_p), - ('ArcNameW', ctypes.c_wchar_p), - ('OpenMode', ctypes.c_uint), - ('OpenResult', ctypes.c_uint), - ('_CmtBuf', ctypes.c_voidp), - ('CmtBufSize', ctypes.c_uint), - ('CmtSize', ctypes.c_uint), - ('CmtState', ctypes.c_uint), - ('Flags', ctypes.c_uint), - ('Reserved', ctypes.c_uint*32), - ] + ('ArcName', ctypes.c_char_p), + ('ArcNameW', ctypes.c_wchar_p), + ('OpenMode', ctypes.c_uint), + ('OpenResult', ctypes.c_uint), + ('_CmtBuf', ctypes.c_voidp), + ('CmtBufSize', ctypes.c_uint), + ('CmtSize', ctypes.c_uint), + ('CmtState', ctypes.c_uint), + ('Flags', ctypes.c_uint), + ('Reserved', ctypes.c_uint * 32), + ] + class RARHeaderDataEx(ctypes.Structure): def __init__(self): - self.CmtBuf = ctypes.c_buffer(64*1024) - ctypes.Structure.__init__(self, _CmtBuf=ctypes.addressof(self.CmtBuf), CmtBufSize=ctypes.sizeof(self.CmtBuf)) + self.CmtBuf = ctypes.c_buffer(64 * 1024) + ctypes.Structure.__init__( + self, + _CmtBuf=ctypes.addressof(self.CmtBuf), + CmtBufSize=ctypes.sizeof(self.CmtBuf)) _fields_ = [ - ('ArcName', ctypes.c_char*1024), - ('ArcNameW', ctypes.c_wchar*1024), - ('FileName', ctypes.c_char*1024), - ('FileNameW', ctypes.c_wchar*1024), - ('Flags', ctypes.c_uint), - ('PackSize', ctypes.c_uint), - ('PackSizeHigh', ctypes.c_uint), - ('UnpSize', ctypes.c_uint), - ('UnpSizeHigh', ctypes.c_uint), - ('HostOS', ctypes.c_uint), - ('FileCRC', ctypes.c_uint), - ('FileTime', ctypes.c_uint), - ('UnpVer', ctypes.c_uint), - ('Method', ctypes.c_uint), - ('FileAttr', ctypes.c_uint), - ('_CmtBuf', ctypes.c_voidp), - ('CmtBufSize', ctypes.c_uint), - ('CmtSize', ctypes.c_uint), - ('CmtState', ctypes.c_uint), - ('Reserved', ctypes.c_uint*1024), - ] + ('ArcName', ctypes.c_char * 1024), + ('ArcNameW', ctypes.c_wchar * 1024), + ('FileName', ctypes.c_char * 1024), + ('FileNameW', ctypes.c_wchar * 1024), + ('Flags', ctypes.c_uint), + ('PackSize', ctypes.c_uint), + ('PackSizeHigh', ctypes.c_uint), + ('UnpSize', ctypes.c_uint), + ('UnpSizeHigh', ctypes.c_uint), + ('HostOS', ctypes.c_uint), + ('FileCRC', ctypes.c_uint), + ('FileTime', ctypes.c_uint), + ('UnpVer', ctypes.c_uint), + ('Method', ctypes.c_uint), + ('FileAttr', ctypes.c_uint), + ('_CmtBuf', ctypes.c_voidp), + ('CmtBufSize', ctypes.c_uint), + ('CmtSize', ctypes.c_uint), + ('CmtState', ctypes.c_uint), + ('Reserved', ctypes.c_uint * 1024), + ] + def DosDateTimeToTimeTuple(dosDateTime): """Convert an MS-DOS format date time to a Python time tuple. @@ -127,10 +138,12 @@ def DosDateTimeToTimeTuple(dosDateTime): day = dosDate & 0x1f month = (dosDate >> 5) & 0xf year = 1980 + (dosDate >> 9) - second = 2*(dosTime & 0x1f) + second = 2 * (dosTime & 0x1f) minute = (dosTime >> 5) & 0x3f hour = dosTime >> 11 - return time.localtime(time.mktime((year, month, day, hour, minute, second, 0, 1, -1))) + return time.localtime( + time.mktime((year, month, day, hour, minute, second, 0, 1, -1))) + def _wrap(restype, function, argtypes): result = function @@ -138,80 +151,98 @@ def _wrap(restype, function, argtypes): result.restype = restype return result + RARGetDllVersion = _wrap(ctypes.c_int, unrar.RARGetDllVersion, []) -RAROpenArchiveEx = _wrap(ctypes.wintypes.HANDLE, unrar.RAROpenArchiveEx, [ctypes.POINTER(RAROpenArchiveDataEx)]) +RAROpenArchiveEx = _wrap(ctypes.wintypes.HANDLE, unrar.RAROpenArchiveEx, + [ctypes.POINTER(RAROpenArchiveDataEx)]) + +RARReadHeaderEx = _wrap( + ctypes.c_int, unrar.RARReadHeaderEx, + [ctypes.wintypes.HANDLE, + ctypes.POINTER(RARHeaderDataEx)]) + +_RARSetPassword = _wrap(ctypes.c_int, unrar.RARSetPassword, + [ctypes.wintypes.HANDLE, ctypes.c_char_p]) -RARReadHeaderEx = _wrap(ctypes.c_int, unrar.RARReadHeaderEx, [ctypes.wintypes.HANDLE, ctypes.POINTER(RARHeaderDataEx)]) -_RARSetPassword = _wrap(ctypes.c_int, unrar.RARSetPassword, [ctypes.wintypes.HANDLE, ctypes.c_char_p]) def RARSetPassword(*args, **kwargs): _RARSetPassword(*args, **kwargs) -RARProcessFile = _wrap(ctypes.c_int, unrar.RARProcessFile, [ctypes.wintypes.HANDLE, ctypes.c_int, ctypes.c_char_p, ctypes.c_char_p]) -RARCloseArchive = _wrap(ctypes.c_int, unrar.RARCloseArchive, [ctypes.wintypes.HANDLE]) - -UNRARCALLBACK = ctypes.WINFUNCTYPE(ctypes.c_int, ctypes.c_uint, ctypes.c_long, ctypes.c_long, ctypes.c_long) -RARSetCallback = _wrap(ctypes.c_int, unrar.RARSetCallback, [ctypes.wintypes.HANDLE, UNRARCALLBACK, ctypes.c_long]) +RARProcessFile = _wrap( + ctypes.c_int, unrar.RARProcessFile, + [ctypes.wintypes.HANDLE, ctypes.c_int, ctypes.c_char_p, ctypes.c_char_p]) +RARCloseArchive = _wrap(ctypes.c_int, unrar.RARCloseArchive, + [ctypes.wintypes.HANDLE]) +UNRARCALLBACK = ctypes.WINFUNCTYPE(ctypes.c_int, ctypes.c_uint, ctypes.c_long, + ctypes.c_long, ctypes.c_long) +RARSetCallback = _wrap(ctypes.c_int, unrar.RARSetCallback, + [ctypes.wintypes.HANDLE, UNRARCALLBACK, ctypes.c_long]) RARExceptions = { - ERAR_NO_MEMORY : MemoryError, - ERAR_BAD_DATA : ArchiveHeaderBroken, - ERAR_BAD_ARCHIVE : InvalidRARArchive, - ERAR_EOPEN : FileOpenError, - } + ERAR_NO_MEMORY: MemoryError, + ERAR_BAD_DATA: ArchiveHeaderBroken, + ERAR_BAD_ARCHIVE: InvalidRARArchive, + ERAR_EOPEN: FileOpenError, +} + class PassiveReader: """Used for reading files to memory""" - def __init__(self, usercallback = None): + + def __init__(self, usercallback=None): self.buf = [] self.ucb = usercallback - + def _callback(self, msg, UserData, P1, P2): if msg == UCM_PROCESSDATA: - data = (ctypes.c_char*P2).from_address(P1).raw - if self.ucb!=None: + data = (ctypes.c_char * P2).from_address(P1).raw + if self.ucb != None: self.ucb(data) else: self.buf.append(data) return 1 - + def get_result(self): return ''.join(self.buf) + class RarInfoIterator(object): def __init__(self, arc): self.arc = arc self.index = 0 self.headerData = RARHeaderDataEx() - self.res = RARReadHeaderEx(self.arc._handle, ctypes.byref(self.headerData)) - if self.res==ERAR_BAD_DATA: + self.res = RARReadHeaderEx(self.arc._handle, + ctypes.byref(self.headerData)) + if self.res == ERAR_BAD_DATA: raise IncorrectRARPassword self.arc.lockStatus = "locked" self.arc.needskip = False - + def __iter__(self): return self - + def next(self): - if self.index>0: + if self.index > 0: if self.arc.needskip: RARProcessFile(self.arc._handle, RAR_SKIP, None, None) - self.res = RARReadHeaderEx(self.arc._handle, ctypes.byref(self.headerData)) + self.res = RARReadHeaderEx(self.arc._handle, + ctypes.byref(self.headerData)) if self.res: - raise StopIteration + raise StopIteration self.arc.needskip = True - + data = {} data['index'] = self.index data['filename'] = self.headerData.FileName data['datetime'] = DosDateTimeToTimeTuple(self.headerData.FileTime) data['isdir'] = ((self.headerData.Flags & 0xE0) == 0xE0) - data['size'] = self.headerData.UnpSize + (self.headerData.UnpSizeHigh << 32) + data['size'] = self.headerData.UnpSize + ( + self.headerData.UnpSizeHigh << 32) if self.headerData.CmtState == 1: data['comment'] = self.headerData.CmtBuf.value else: @@ -219,24 +250,28 @@ class RarInfoIterator(object): self.index += 1 return data - def __del__(self): self.arc.lockStatus = "finished" + def generate_password_provider(password): def password_provider_callback(msg, UserData, P1, P2): - if msg == UCM_NEEDPASSWORD and password!=None: - (ctypes.c_char*P2).from_address(P1).value = password + if msg == UCM_NEEDPASSWORD and password != None: + (ctypes.c_char * P2).from_address(P1).value = password return 1 + return password_provider_callback -class RarFileImplementation(object): - def init(self, password=None): +class RarFileImplementation(object): + def init(self, archiveName = '', password=None): + self.archiveName = archiveName self.password = password - archiveData = RAROpenArchiveDataEx(ArcNameW=self.archiveName, OpenMode=RAR_OM_EXTRACT) + archiveData = RAROpenArchiveDataEx( + ArcNameW=self.archiveName, OpenMode=RAR_OM_EXTRACT) self._handle = RAROpenArchiveEx(ctypes.byref(archiveData)) - self.c_callback = UNRARCALLBACK(generate_password_provider(self.password)) + self.c_callback = UNRARCALLBACK( + generate_password_provider(self.password)) RARSetCallback(self._handle, self.c_callback, 1) if archiveData.OpenResult != 0: @@ -249,10 +284,8 @@ class RarFileImplementation(object): if password: RARSetPassword(self._handle, password) - - self.lockStatus = "ready" - + self.lockStatus = "ready" def destruct(self): if self._handle and RARCloseArchive: @@ -260,7 +293,8 @@ class RarFileImplementation(object): def make_sure_ready(self): if self.lockStatus == "locked": - raise InvalidRARArchiveUsage("cannot execute infoiter() without finishing previous one") + raise InvalidRARArchiveUsage( + "cannot execute infoiter() without finishing previous one") if self.lockStatus == "finished": self.destruct() self.init(self.password) @@ -277,33 +311,32 @@ class RarFileImplementation(object): c_callback = UNRARCALLBACK(reader._callback) RARSetCallback(self._handle, c_callback, 1) tmpres = RARProcessFile(self._handle, RAR_TEST, None, None) - if tmpres==ERAR_BAD_DATA: + if tmpres == ERAR_BAD_DATA: raise IncorrectRARPassword self.needskip = False res.append((info, reader.get_result())) return res - - def extract(self, checker, path, withSubpath, overwrite): + def extract(self, checker, path, withSubpath, overwrite): res = [] for info in self.infoiter(): checkres = checker(info) - if checkres!=False and not info.isdir: - if checkres==True: + if checkres != False and not info.isdir: + if checkres == True: fn = info.filename if not withSubpath: fn = os.path.split(fn)[-1] target = os.path.join(path, fn) else: - raise DeprecationWarning, "Condition callbacks returning strings are deprecated and only supported in Windows" target = checkres + raise DeprecationWarning("Condition callbacks returning strings are deprecated and only supported in Windows") + if overwrite or (not os.path.exists(target)): - tmpres = RARProcessFile(self._handle, RAR_EXTRACT, None, target) - if tmpres==ERAR_BAD_DATA: + tmpres = RARProcessFile(self._handle, RAR_EXTRACT, None, + target) + if tmpres == ERAR_BAD_DATA: raise IncorrectRARPassword self.needskip = False res.append(info) return res - - diff --git a/comet.py b/comet.py old mode 100644 new mode 100755 index dca1f31..fc0a00e --- a/comet.py +++ b/comet.py @@ -1,8 +1,6 @@ """ A python class to encapsulate CoMet data -""" -""" Copyright 2012-2014 Anthony Beville Licensed under the Apache License, Version 2.0 (the "License"); @@ -20,241 +18,241 @@ limitations under the License. from datetime import datetime import zipfile -from pprint import pprint +from pprint import pprint import xml.etree.ElementTree as ET from comicapi.genericmetadata import GenericMetadata import comicapi.utils + class CoMet: - - writer_synonyms = ['writer', 'plotter', 'scripter'] - penciller_synonyms = [ 'artist', 'penciller', 'penciler', 'breakdowns' ] - inker_synonyms = [ 'inker', 'artist', 'finishes' ] - colorist_synonyms = [ 'colorist', 'colourist', 'colorer', 'colourer' ] - letterer_synonyms = [ 'letterer'] - cover_synonyms = [ 'cover', 'covers', 'coverartist', 'cover artist' ] - editor_synonyms = [ 'editor'] - - def metadataFromString( self, string ): - tree = ET.ElementTree(ET.fromstring( string )) - return self.convertXMLToMetadata( tree ) + writer_synonyms = ['writer', 'plotter', 'scripter'] + penciller_synonyms = ['artist', 'penciller', 'penciler', 'breakdowns'] + inker_synonyms = ['inker', 'artist', 'finishes'] + colorist_synonyms = ['colorist', 'colourist', 'colorer', 'colourer'] + letterer_synonyms = ['letterer'] + cover_synonyms = ['cover', 'covers', 'coverartist', 'cover artist'] + editor_synonyms = ['editor'] - def stringFromMetadata( self, metadata ): + def metadataFromString(self, string): - header = '\n' - - tree = self.convertMetadataToXML( self, metadata ) - return header + ET.tostring(tree.getroot()) + tree = ET.ElementTree(ET.fromstring(string)) + return self.convertXMLToMetadata(tree) - def indent( self, elem, level=0 ): - # for making the XML output readable - i = "\n" + level*" " - if len(elem): - if not elem.text or not elem.text.strip(): - elem.text = i + " " - if not elem.tail or not elem.tail.strip(): - elem.tail = i - for elem in elem: - self.indent( elem, level+1 ) - if not elem.tail or not elem.tail.strip(): - elem.tail = i - else: - if level and (not elem.tail or not elem.tail.strip()): - elem.tail = i - - def convertMetadataToXML( self, filename, metadata ): + def stringFromMetadata(self, metadata): - #shorthand for the metadata - md = metadata + header = '\n' - # build a tree structure - root = ET.Element("comet") - root.attrib['xmlns:comet'] = "http://www.denvog.com/comet/" - root.attrib['xmlns:xsi'] = "http://www.w3.org/2001/XMLSchema-instance" - root.attrib['xsi:schemaLocation'] = "http://www.denvog.com http://www.denvog.com/comet/comet.xsd" - - #helper func - def assign( comet_entry, md_entry): - if md_entry is not None: - ET.SubElement(root, comet_entry).text = u"{0}".format(md_entry) - - # title is manditory - if md.title is None: - md.title = "" - assign( 'title', md.title ) - assign( 'series', md.series ) - assign( 'issue', md.issue ) #must be int?? - assign( 'volume', md.volume ) - assign( 'description', md.comments ) - assign( 'publisher', md.publisher ) - assign( 'pages', md.pageCount ) - assign( 'format', md.format ) - assign( 'language', md.language ) - assign( 'rating', md.maturityRating ) - assign( 'price', md.price ) - assign( 'isVersionOf', md.isVersionOf ) - assign( 'rights', md.rights ) - assign( 'identifier', md.identifier ) - assign( 'lastMark', md.lastMark ) - assign( 'genre', md.genre ) # TODO repeatable + tree = self.convertMetadataToXML(self, metadata) + return header + ET.tostring(tree.getroot()) - if md.characters is not None: - char_list = [ c.strip() for c in md.characters.split(',') ] - for c in char_list: - assign( 'character', c ) - - if md.manga is not None and md.manga == "YesAndRightToLeft": - assign( 'readingDirection', "rtl") - - date_str = "" - if md.year is not None: - date_str = str(md.year).zfill(4) - if md.month is not None: - date_str += "-" + str(md.month).zfill(2) - assign( 'date', date_str ) + def indent(self, elem, level=0): + # for making the XML output readable + i = "\n" + level * " " + if len(elem): + if not elem.text or not elem.text.strip(): + elem.text = i + " " + if not elem.tail or not elem.tail.strip(): + elem.tail = i + for elem in elem: + self.indent(elem, level + 1) + if not elem.tail or not elem.tail.strip(): + elem.tail = i + else: + if level and (not elem.tail or not elem.tail.strip()): + elem.tail = i - assign( 'coverImage', md.coverImage ) + def convertMetadataToXML(self, filename, metadata): - # need to specially process the credits, since they are structured differently than CIX - credit_writer_list = list() - credit_penciller_list = list() - credit_inker_list = list() - credit_colorist_list = list() - credit_letterer_list = list() - credit_cover_list = list() - credit_editor_list = list() - - # loop thru credits, and build a list for each role that CoMet supports - for credit in metadata.credits: + #shorthand for the metadata + md = metadata - if credit['role'].lower() in set( self.writer_synonyms ): - ET.SubElement(root, 'writer').text = u"{0}".format(credit['person']) + # build a tree structure + root = ET.Element("comet") + root.attrib['xmlns:comet'] = "http://www.denvog.com/comet/" + root.attrib['xmlns:xsi'] = "http://www.w3.org/2001/XMLSchema-instance" + root.attrib[ + 'xsi:schemaLocation'] = "http://www.denvog.com http://www.denvog.com/comet/comet.xsd" - if credit['role'].lower() in set( self.penciller_synonyms ): - ET.SubElement(root, 'penciller').text = u"{0}".format(credit['person']) - - if credit['role'].lower() in set( self.inker_synonyms ): - ET.SubElement(root, 'inker').text = u"{0}".format(credit['person']) - - if credit['role'].lower() in set( self.colorist_synonyms ): - ET.SubElement(root, 'colorist').text = u"{0}".format(credit['person']) + #helper func + def assign(comet_entry, md_entry): + if md_entry is not None: + ET.SubElement(root, comet_entry).text = u"{0}".format(md_entry) - if credit['role'].lower() in set( self.letterer_synonyms ): - ET.SubElement(root, 'letterer').text = u"{0}".format(credit['person']) + # title is manditory + if md.title is None: + md.title = "" + assign('title', md.title) + assign('series', md.series) + assign('issue', md.issue) #must be int?? + assign('volume', md.volume) + assign('description', md.comments) + assign('publisher', md.publisher) + assign('pages', md.pageCount) + assign('format', md.format) + assign('language', md.language) + assign('rating', md.maturityRating) + assign('price', md.price) + assign('isVersionOf', md.isVersionOf) + assign('rights', md.rights) + assign('identifier', md.identifier) + assign('lastMark', md.lastMark) + assign('genre', md.genre) # TODO repeatable - if credit['role'].lower() in set( self.cover_synonyms ): - ET.SubElement(root, 'coverDesigner').text = u"{0}".format(credit['person']) + if md.characters is not None: + char_list = [c.strip() for c in md.characters.split(',')] + for c in char_list: + assign('character', c) - if credit['role'].lower() in set( self.editor_synonyms ): - ET.SubElement(root, 'editor').text = u"{0}".format(credit['person']) - + if md.manga is not None and md.manga == "YesAndRightToLeft": + assign('readingDirection', "rtl") - # self pretty-print - self.indent(root) + date_str = "" + if md.year is not None: + date_str = str(md.year).zfill(4) + if md.month is not None: + date_str += "-" + str(md.month).zfill(2) + assign('date', date_str) - # wrap it in an ElementTree instance, and save as XML - tree = ET.ElementTree(root) - return tree - + assign('coverImage', md.coverImage) - def convertXMLToMetadata( self, tree ): - - root = tree.getroot() + # need to specially process the credits, since they are structured differently than CIX + credit_writer_list = list() + credit_penciller_list = list() + credit_inker_list = list() + credit_colorist_list = list() + credit_letterer_list = list() + credit_cover_list = list() + credit_editor_list = list() - if root.tag != 'comet': - raise 1 - return None + # loop thru credits, and build a list for each role that CoMet supports + for credit in metadata.credits: - metadata = GenericMetadata() - md = metadata - - # Helper function - def xlate( tag ): - node = root.find( tag ) - if node is not None: - return node.text - else: - return None - - md.series = xlate( 'series' ) - md.title = xlate( 'title' ) - md.issue = xlate( 'issue' ) - md.volume = xlate( 'volume' ) - md.comments = xlate( 'description' ) - md.publisher = xlate( 'publisher' ) - md.language = xlate( 'language' ) - md.format = xlate( 'format' ) - md.pageCount = xlate( 'pages' ) - md.maturityRating = xlate( 'rating' ) - md.price = xlate( 'price' ) - md.isVersionOf = xlate( 'isVersionOf' ) - md.rights = xlate( 'rights' ) - md.identifier = xlate( 'identifier' ) - md.lastMark = xlate( 'lastMark' ) - md.genre = xlate( 'genre' ) # TODO - repeatable field + if credit['role'].lower() in set(self.writer_synonyms): + ET.SubElement(root, 'writer').text = u"{0}".format( + credit['person']) - date = xlate( 'date' ) - if date is not None: - parts = date.split('-') - if len( parts) > 0: - md.year = parts[0] - if len( parts) > 1: - md.month = parts[1] - - md.coverImage = xlate( 'coverImage' ) - - readingDirection = xlate( 'readingDirection' ) - if readingDirection is not None and readingDirection == "rtl": - md.manga = "YesAndRightToLeft" - - # loop for character tags - char_list = [] - for n in root: - if n.tag == 'character': - char_list.append(n.text.strip()) - md.characters = comicapi.utils.listToString( char_list ) + if credit['role'].lower() in set(self.penciller_synonyms): + ET.SubElement(root, 'penciller').text = u"{0}".format( + credit['person']) - # Now extract the credit info - for n in root: - if ( n.tag == 'writer' or - n.tag == 'penciller' or - n.tag == 'inker' or - n.tag == 'colorist' or - n.tag == 'letterer' or - n.tag == 'editor' - ): - metadata.addCredit( n.text.strip(), n.tag.title() ) + if credit['role'].lower() in set(self.inker_synonyms): + ET.SubElement(root, 'inker').text = u"{0}".format( + credit['person']) - if n.tag == 'coverDesigner': - metadata.addCredit( n.text.strip(), "Cover" ) + if credit['role'].lower() in set(self.colorist_synonyms): + ET.SubElement(root, 'colorist').text = u"{0}".format( + credit['person']) + if credit['role'].lower() in set(self.letterer_synonyms): + ET.SubElement(root, 'letterer').text = u"{0}".format( + credit['person']) - metadata.isEmpty = False - - return metadata + if credit['role'].lower() in set(self.cover_synonyms): + ET.SubElement(root, 'coverDesigner').text = u"{0}".format( + credit['person']) - #verify that the string actually contains CoMet data in XML format - def validateString( self, string ): - try: - tree = ET.ElementTree(ET.fromstring( string )) - root = tree.getroot() - if root.tag != 'comet': - raise Exception - except: - return False - - return True + if credit['role'].lower() in set(self.editor_synonyms): + ET.SubElement(root, 'editor').text = u"{0}".format( + credit['person']) + # self pretty-print + self.indent(root) - def writeToExternalFile( self, filename, metadata ): - - tree = self.convertMetadataToXML( self, metadata ) - #ET.dump(tree) - tree.write(filename, encoding='utf-8') - - def readFromExternalFile( self, filename ): + # wrap it in an ElementTree instance, and save as XML + tree = ET.ElementTree(root) + return tree - tree = ET.parse( filename ) - return self.convertXMLToMetadata( tree ) + def convertXMLToMetadata(self, tree): + root = tree.getroot() + + if root.tag != 'comet': + raise KeyError("Not a comet XML!") + #return None + + metadata = GenericMetadata() + md = metadata + + # Helper function + def xlate(tag): + node = root.find(tag) + if node is not None: + return node.text + else: + return None + + md.series = xlate('series') + md.title = xlate('title') + md.issue = xlate('issue') + md.volume = xlate('volume') + md.comments = xlate('description') + md.publisher = xlate('publisher') + md.language = xlate('language') + md.format = xlate('format') + md.pageCount = xlate('pages') + md.maturityRating = xlate('rating') + md.price = xlate('price') + md.isVersionOf = xlate('isVersionOf') + md.rights = xlate('rights') + md.identifier = xlate('identifier') + md.lastMark = xlate('lastMark') + md.genre = xlate('genre') # TODO - repeatable field + + date = xlate('date') + if date is not None: + parts = date.split('-') + if len(parts) > 0: + md.year = parts[0] + if len(parts) > 1: + md.month = parts[1] + + md.coverImage = xlate('coverImage') + + readingDirection = xlate('readingDirection') + if readingDirection is not None and readingDirection == "rtl": + md.manga = "YesAndRightToLeft" + + # loop for character tags + char_list = [] + for n in root: + if n.tag == 'character': + char_list.append(n.text.strip()) + md.characters = comicapi.utils.listToString(char_list) + + # Now extract the credit info + for n in root: + if (n.tag == 'writer' or n.tag == 'penciller' or n.tag == 'inker' + or n.tag == 'colorist' or n.tag == 'letterer' + or n.tag == 'editor'): + metadata.addCredit(n.text.strip(), n.tag.title()) + + if n.tag == 'coverDesigner': + metadata.addCredit(n.text.strip(), "Cover") + + metadata.isEmpty = False + + return metadata + + #verify that the string actually contains CoMet data in XML format + def validateString(self, string): + try: + tree = ET.ElementTree(ET.fromstring(string)) + root = tree.getroot() + if root.tag != 'comet': + raise Exception + except: + return False + + return True + + def writeToExternalFile(self, filename, metadata): + + tree = self.convertMetadataToXML(self, metadata) + #ET.dump(tree) + tree.write(filename, encoding='utf-8') + + def readFromExternalFile(self, filename): + + tree = ET.parse(filename) + return self.convertXMLToMetadata(tree) diff --git a/comicarchive.py b/comicarchive.py old mode 100644 new mode 100755 index 51f15c7..8d1a170 --- a/comicarchive.py +++ b/comicarchive.py @@ -1,8 +1,6 @@ """ A python class to represent a single comic, be it file or folder of images -""" -""" Copyright 2012-2014 Anthony Beville Licensed under the Apache License, Version 2.0 (the "License"); @@ -36,20 +34,24 @@ import ctypes import io from unrar import constants + class OpenableRarFile(rarfile.RarFile): def open(self, member): #print "opening %s..." % member # based on https://github.com/matiasb/python-unrar/pull/4/files if isinstance(member, rarfile.RarInfo): member = member.filename - archive = unrarlib.RAROpenArchiveDataEx(self.filename, mode=constants.RAR_OM_EXTRACT) + archive = unrarlib.RAROpenArchiveDataEx( + self.filename, mode=constants.RAR_OM_EXTRACT) handle = self._open(archive) found, buf = False, [] + def _callback(msg, UserData, P1, P2): if msg == constants.UCM_PROCESSDATA: - data = (ctypes.c_char*P2).from_address(P1).raw + data = (ctypes.c_char * P2).from_address(P1).raw buf.append(data) return 1 + c_callback = unrarlib.UNRARCALLBACK(_callback) unrarlib.RARSetCallback(handle, c_callback, 1) try: @@ -77,13 +79,13 @@ import time from io import StringIO from io import BytesIO -try: +try: from PIL import Image pil_available = True except ImportError: pil_available = False -sys.path.insert(0, os.path.abspath(".") ) +sys.path.insert(0, os.path.abspath(".")) #import UnRAR2 #from UnRAR2.rar_exceptions import * @@ -95,95 +97,100 @@ from comicapi.genericmetadata import GenericMetadata, PageType from comicapi.filenameparser import FileNameParser from PyPDF2 import PdfFileReader + class MetaDataStyle: CBI = 0 CIX = 1 COMET = 2 - name = [ 'ComicBookLover', 'ComicRack', 'CoMet' ] + name = ['ComicBookLover', 'ComicRack', 'CoMet'] + class ZipArchiver: - - def __init__( self, path ): + def __init__(self, path): self.path = path - def getArchiveComment( self ): - zf = zipfile.ZipFile( self.path, 'r' ) + def getArchiveComment(self): + zf = zipfile.ZipFile(self.path, 'r') comment = zf.comment zf.close() return comment - def setArchiveComment( self, comment ): - return self.writeZipComment( self.path, comment ) + def setArchiveComment(self, comment): + return self.writeZipComment(self.path, comment) - def readArchiveFile( self, archive_file ): + def readArchiveFile(self, archive_file): data = "" - zf = zipfile.ZipFile( self.path, 'r' ) + zf = zipfile.ZipFile(self.path, 'r') try: - data = zf.read( archive_file ) + data = zf.read(archive_file) except zipfile.BadZipfile as e: - errMsg=u"bad zipfile [{0}]: {1} :: {2}".format(e, self.path, archive_file) - sys.stderr.buffer.write(bytes(errMsg,"UTF-8")) + errMsg = u"bad zipfile [{0}]: {1} :: {2}".format( + e, self.path, archive_file) + sys.stderr.buffer.write(bytes(errMsg, "UTF-8")) zf.close() raise IOError except Exception as e: zf.close() - errMsg=u"bad zipfile [{0}]: {1} :: {2}".format(e, self.path, archive_file) - sys.stderr.buffer.write(bytes(errMsg,"UTF-8")) + errMsg = u"bad zipfile [{0}]: {1} :: {2}".format( + e, self.path, archive_file) + sys.stderr.buffer.write(bytes(errMsg, "UTF-8")) raise IOError finally: zf.close() return data - def removeArchiveFile( self, archive_file ): + def removeArchiveFile(self, archive_file): try: - self.rebuildZipFile( [ archive_file ] ) + self.rebuildZipFile([archive_file]) except: return False else: return True - def writeArchiveFile( self, archive_file, data ): + def writeArchiveFile(self, archive_file, data): # At the moment, no other option but to rebuild the whole # zip archive w/o the indicated file. Very sucky, but maybe # another solution can be found try: - self.rebuildZipFile( [ archive_file ] ) + self.rebuildZipFile([archive_file]) #now just add the archive file as a new one - zf = zipfile.ZipFile(self.path, mode='a', compression=zipfile.ZIP_DEFLATED ) - zf.writestr( archive_file, data ) + zf = zipfile.ZipFile( + self.path, mode='a', compression=zipfile.ZIP_DEFLATED) + zf.writestr(archive_file, data) zf.close() return True except: return False - def getArchiveFilenameList( self ): + def getArchiveFilenameList(self): try: - zf = zipfile.ZipFile( self.path, 'r' ) + zf = zipfile.ZipFile(self.path, 'r') namelist = zf.namelist() zf.close() return namelist except Exception as e: - errMsg=u"Unable to get zipfile list [{0}]: {1}".format(e, self.path) - sys.stderr.buffer.write(bytes(errMsg,"UTF-8")) + errMsg = u"Unable to get zipfile list [{0}]: {1}".format( + e, self.path) + sys.stderr.buffer.write(bytes(errMsg, "UTF-8")) return [] # zip helper func - def rebuildZipFile( self, exclude_list ): + def rebuildZipFile(self, exclude_list): # this recompresses the zip archive, without the files in the exclude_list #errMsg=u"Rebuilding zip {0} without {1}".format( self.path, exclude_list ) # generate temp file - tmp_fd, tmp_name = tempfile.mkstemp( dir=os.path.dirname(self.path) ) - os.close( tmp_fd ) + tmp_fd, tmp_name = tempfile.mkstemp(dir=os.path.dirname(self.path)) + os.close(tmp_fd) - zin = zipfile.ZipFile (self.path, 'r') - zout = zipfile.ZipFile (tmp_name, 'w') + zin = zipfile.ZipFile(self.path, 'r') + zout = zipfile.ZipFile(tmp_name, 'w') for item in zin.infolist(): buffer = zin.read(item.filename) - if ( item.filename not in exclude_list ): + if (item.filename not in exclude_list): zout.writestr(item, buffer) #preserve the old comment @@ -193,11 +200,10 @@ class ZipArchiver: zin.close() # replace with the new file - os.remove( self.path ) - os.rename( tmp_name, self.path ) + os.remove(self.path) + os.rename(tmp_name, self.path) - - def writeZipComment( self, filename, comment ): + def writeZipComment(self, filename, comment): """ This is a custom function for writing a comment to a zip file, since the built-in one doesn't seem to work on Windows and Mac OS/X @@ -221,14 +227,14 @@ class ZipArchiver: value = bytearray() # walk backwards to find the "End of Central Directory" record - while ( not found ) and ( -pos != file_length ): + while (not found) and (-pos != file_length): # seek, relative to EOF - fo.seek( pos, 2) + fo.seek(pos, 2) - value = fo.read( 4 ) + value = fo.read(4) #look for the end of central directory signature - if bytearray(value) == bytearray([ 0x50, 0x4b, 0x05, 0x06 ]): + if bytearray(value) == bytearray([0x50, 0x4b, 0x05, 0x06]): found = True else: # not found, step back another byte @@ -239,18 +245,19 @@ class ZipArchiver: # now skip forward 20 bytes to the comment length word pos += 20 - fo.seek( pos, 2) + fo.seek(pos, 2) # Pack the length of the comment string - format = "H" # one 2-byte integer - comment_length = struct.pack(format, len(comment)) # pack integer in a binary string + format = "H" # one 2-byte integer + comment_length = struct.pack( + format, len(comment)) # pack integer in a binary string # write out the length - fo.write( comment_length ) - fo.seek( pos+2, 2) + fo.write(comment_length) + fo.seek(pos + 2, 2) # write out the comment itself - fo.write( comment ) + fo.write(comment) fo.truncate() fo.close() else: @@ -260,24 +267,24 @@ class ZipArchiver: else: return True - def copyFromArchive( self, otherArchive ): + def copyFromArchive(self, otherArchive): # Replace the current zip with one copied from another archive try: - zout = zipfile.ZipFile (self.path, 'w') + zout = zipfile.ZipFile(self.path, 'w') for fname in otherArchive.getArchiveFilenameList(): - data = otherArchive.readArchiveFile( fname ) + data = otherArchive.readArchiveFile(fname) if data is not None: - zout.writestr( fname, data ) + zout.writestr(fname, data) zout.close() #preserve the old comment comment = otherArchive.getArchiveComment() if comment is not None: - if not self.writeZipComment( self.path, comment ): + if not self.writeZipComment(self.path, comment): return False - except Exception as e: - errMsg=u"Error while copying to {0}: {1}".format(self.path, e) - sys.stderr.buffer.write(bytes(errMsg,"UTF-8")) + except Exception as e: + errMsg = u"Error while copying to {0}: {1}".format(self.path, e) + sys.stderr.buffer.write(bytes(errMsg, "UTF-8")) return False else: return True @@ -286,10 +293,12 @@ class ZipArchiver: #------------------------------------------ # RAR implementation + class RarArchiver: devnull = None - def __init__( self, path, rar_exe_path ): + + def __init__(self, path, rar_exe_path): self.path = path self.rar_exe_path = rar_exe_path @@ -298,8 +307,8 @@ class RarArchiver: # windows only, keeps the cmd.exe from popping up if platform.system() == "Windows": - # self.startupinfo = subprocess.STARTUPINFO() - # self.startupinfo.dwFlags |= _subprocess.STARTF_USESHOWWINDOW + # self.startupinfo = subprocess.STARTUPINFO() + # self.startupinfo.dwFlags |= _subprocess.STARTF_USESHOWWINDOW self.startupinfo = None else: self.startupinfo = None @@ -308,32 +317,36 @@ class RarArchiver: #RarArchiver.devnull.close() pass - def getArchiveComment( self ): + def getArchiveComment(self): rarc = self.getRARObj() return rarc.comment - def setArchiveComment( self, comment ): + def setArchiveComment(self, comment): if self.rar_exe_path is not None: try: # write comment to temp file tmp_fd, tmp_name = tempfile.mkstemp() f = os.fdopen(tmp_fd, 'w+b') - f.write( comment ) + f.write(comment) f.close() - working_dir = os.path.dirname( os.path.abspath( self.path ) ) + working_dir = os.path.dirname(os.path.abspath(self.path)) # use external program to write comment to Rar archive - subprocess.call([self.rar_exe_path, 'c', '-w' + working_dir , '-c-', '-z' + tmp_name, self.path], + subprocess.call( + [ + self.rar_exe_path, 'c', '-w' + working_dir, '-c-', + '-z' + tmp_name, self.path + ], startupinfo=self.startupinfo, stdout=RarArchiver.devnull) if platform.system() == "Darwin": time.sleep(1) - os.remove( tmp_name) + os.remove(tmp_name) except: return False else: @@ -341,7 +354,7 @@ class RarArchiver: else: return False - def readArchiveFile( self, archive_file ): + def readArchiveFile(self, archive_file): # Make sure to escape brackets, since some funky stuff is going on # underneath with "fnmatch" @@ -353,7 +366,7 @@ class RarArchiver: tries = 0 while tries < 7: try: - tries = tries+1 + tries = tries + 1 #tmp_folder = tempfile.mkdtemp() #tmp_file = os.path.join(tmp_folder, archive_file) #rarc.extract(archive_file, tmp_folder) @@ -361,32 +374,34 @@ class RarArchiver: #data = open(tmp_file).read() entries = [(rarc.getinfo(archive_file), data)] - #shutil.rmtree(tmp_folder, ignore_errors=True) #entries = rarc.read_files( archive_file ) if entries[0][0].file_size != len(entries[0][1]): - errMsg=u"readArchiveFile(): [file is not expected size: {0} vs {1}] {2}:{3} [attempt # {4}]".format( - entries[0][0].file_size,len(entries[0][1]), self.path, archive_file, tries) - sys.stderr.buffer.write(bytes(errMsg,"UTF-8")) + errMsg = u"readArchiveFile(): [file is not expected size: {0} vs {1}] {2}:{3} [attempt # {4}]".format( + entries[0][0].file_size, len(entries[0][1]), self.path, + archive_file, tries) + sys.stderr.buffer.write(bytes(errMsg, "UTF-8")) continue except (OSError, IOError) as e: - errMsg=u"readArchiveFile(): [{0}] {1}:{2} attempt#{3}".format(str(e), self.path, archive_file, tries) - sys.stderr.buffer.write(bytes(errMsg,"UTF-8")) + errMsg = u"readArchiveFile(): [{0}] {1}:{2} attempt#{3}".format( + str(e), self.path, archive_file, tries) + sys.stderr.buffer.write(bytes(errMsg, "UTF-8")) time.sleep(1) except Exception as e: - errMsg=u"Unexpected exception in readArchiveFile(): [{0}] for {1}:{2} attempt#{3}".format(str(e), self.path, archive_file, tries) - sys.stderr.buffer.write(bytes(errMsg,"UTF-8")) + errMsg = u"Unexpected exception in readArchiveFile(): [{0}] for {1}:{2} attempt#{3}".format( + str(e), self.path, archive_file, tries) + sys.stderr.buffer.write(bytes(errMsg, "UTF-8")) break else: #Success" #entries is a list of of tuples: ( rarinfo, filedata) if tries > 1: - errMsg=u"Attempted read_files() {0} times".format(tries) - sys.stderr.buffer.write(bytes(errMsg,"UTF-8")) + errMsg = u"Attempted read_files() {0} times".format(tries) + sys.stderr.buffer.write(bytes(errMsg, "UTF-8")) if (len(entries) == 1): return entries[0][1] else: @@ -394,33 +409,35 @@ class RarArchiver: raise IOError - - - def writeArchiveFile( self, archive_file, data ): + def writeArchiveFile(self, archive_file, data): if self.rar_exe_path is not None: try: tmp_folder = tempfile.mkdtemp() - tmp_file = os.path.join( tmp_folder, archive_file ) + tmp_file = os.path.join(tmp_folder, archive_file) - working_dir = os.path.dirname( os.path.abspath( self.path ) ) + working_dir = os.path.dirname(os.path.abspath(self.path)) # TODO: will this break if 'archive_file' is in a subfolder. i.e. "foo/bar.txt" # will need to create the subfolder above, I guess... f = open(tmp_file, 'w') - f.write( data ) + f.write(data) f.close() # use external program to write file to Rar archive - subprocess.call([self.rar_exe_path, 'a', '-w' + working_dir ,'-c-', '-ep', self.path, tmp_file], + subprocess.call( + [ + self.rar_exe_path, 'a', '-w' + working_dir, '-c-', + '-ep', self.path, tmp_file + ], startupinfo=self.startupinfo, stdout=RarArchiver.devnull) if platform.system() == "Darwin": time.sleep(1) - os.remove( tmp_file) - os.rmdir( tmp_folder) + os.remove(tmp_file) + os.rmdir(tmp_folder) except: return False else: @@ -428,11 +445,12 @@ class RarArchiver: else: return False - def removeArchiveFile( self, archive_file ): + def removeArchiveFile(self, archive_file): if self.rar_exe_path is not None: try: # use external program to remove file from Rar archive - subprocess.call([self.rar_exe_path, 'd','-c-', self.path, archive_file], + subprocess.call( + [self.rar_exe_path, 'd', '-c-', self.path, archive_file], startupinfo=self.startupinfo, stdout=RarArchiver.devnull) @@ -445,7 +463,7 @@ class RarArchiver: else: return False - def getArchiveFilenameList( self ): + def getArchiveFilenameList(self): rarc = self.getRARObj() #namelist = [ item.filename for item in rarc.infolist() ] @@ -454,16 +472,17 @@ class RarArchiver: tries = 0 while tries < 7: try: - tries = tries+1 + tries = tries + 1 #namelist = [ item.filename for item in rarc.infolist() ] namelist = [] for item in rarc.infolist(): if item.file_size != 0: - namelist.append( item.filename ) + namelist.append(item.filename) except (OSError, IOError) as e: - errMsg=u"getArchiveFilenameList(): [{0}] {1} attempt#{2}".format(str(e), self.path, tries) - sys.stderr.buffer.write(bytes(errMsg,"UTF-8")) + errMsg = u"getArchiveFilenameList(): [{0}] {1} attempt#{2}".format( + str(e), self.path, tries) + sys.stderr.buffer.write(bytes(errMsg, "UTF-8")) time.sleep(1) else: @@ -472,18 +491,18 @@ class RarArchiver: raise e - - def getRARObj( self ): + def getRARObj(self): tries = 0 while tries < 7: try: - tries = tries+1 + tries = tries + 1 #rarc = UnRAR2.RarFile( self.path ) rarc = OpenableRarFile(self.path) except (OSError, IOError) as e: - errMsg=u"getRARObj(): [{0}] {1} attempt#{2}".format(str(e), self.path, tries) - sys.stderr.buffer.write(bytes(errMsg,"UTF-8")) + errMsg = u"getRARObj(): [{0}] {1} attempt#{2}".format( + str(e), self.path, tries) + sys.stderr.buffer.write(bytes(errMsg, "UTF-8")) time.sleep(1) else: @@ -492,26 +511,26 @@ class RarArchiver: raise e + #------------------------------------------ # Folder implementation class FolderArchiver: - - def __init__( self, path ): + def __init__(self, path): self.path = path self.comment_file_name = "ComicTaggerFolderComment.txt" - def getArchiveComment( self ): - return self.readArchiveFile( self.comment_file_name ) + def getArchiveComment(self): + return self.readArchiveFile(self.comment_file_name) - def setArchiveComment( self, comment ): - return self.writeArchiveFile( self.comment_file_name, comment ) + def setArchiveComment(self, comment): + return self.writeArchiveFile(self.comment_file_name, comment) - def readArchiveFile( self, archive_file ): + def readArchiveFile(self, archive_file): data = "" - fname = os.path.join( self.path, archive_file ) + fname = os.path.join(self.path, archive_file) try: - with open( fname, 'rb' ) as f: + with open(fname, 'rb') as f: data = f.read() f.close() except IOError as e: @@ -519,83 +538,98 @@ class FolderArchiver: return data - def writeArchiveFile( self, archive_file, data ): + def writeArchiveFile(self, archive_file, data): - fname = os.path.join( self.path, archive_file ) + fname = os.path.join(self.path, archive_file) try: with open(fname, 'w+') as f: - f.write( data ) + f.write(data) f.close() except: return False else: return True - def removeArchiveFile( self, archive_file ): + def removeArchiveFile(self, archive_file): - fname = os.path.join( self.path, archive_file ) + fname = os.path.join(self.path, archive_file) try: - os.remove( fname ) + os.remove(fname) except: return False else: return True - def getArchiveFilenameList( self ): - return self.listFiles( self.path ) + def getArchiveFilenameList(self): + return self.listFiles(self.path) - def listFiles( self, folder ): + def listFiles(self, folder): itemlist = list() - for item in os.listdir( folder ): - itemlist.append( item ) - if os.path.isdir( item ): - itemlist.extend( self.listFiles( os.path.join( folder, item ) )) + for item in os.listdir(folder): + itemlist.append(item) + if os.path.isdir(item): + itemlist.extend(self.listFiles(os.path.join(folder, item))) return itemlist + #------------------------------------------ # Unknown implementation class UnknownArchiver: - - def __init__( self, path ): + def __init__(self, path): self.path = path - def getArchiveComment( self ): + def getArchiveComment(self): return "" - def setArchiveComment( self, comment ): + + def setArchiveComment(self, comment): return False - def readArchiveFile( self ): + + def readArchiveFile(self): return "" - def writeArchiveFile( self, archive_file, data ): + + def writeArchiveFile(self, archive_file, data): return False - def removeArchiveFile( self, archive_file ): + + def removeArchiveFile(self, archive_file): return False - def getArchiveFilenameList( self ): + + def getArchiveFilenameList(self): return [] + class PdfArchiver: - def __init__( self, path ): + def __init__(self, path): self.path = path - def getArchiveComment( self ): + def getArchiveComment(self): return "" - def setArchiveComment( self, comment ): + + def setArchiveComment(self, comment): return False - def readArchiveFile( self, page_num ): - return subprocess.check_output(['mudraw', '-o','-', self.path, str(int(os.path.basename(page_num)[:-4]))]) - def writeArchiveFile( self, archive_file, data ): + + def readArchiveFile(self, page_num): + return subprocess.check_output([ + 'mudraw', '-o', '-', self.path, + str(int(os.path.basename(page_num)[:-4])) + ]) + + def writeArchiveFile(self, archive_file, data): return False - def removeArchiveFile( self, archive_file ): + + def removeArchiveFile(self, archive_file): return False - def getArchiveFilenameList( self ): + + def getArchiveFilenameList(self): out = [] pdf = PdfFileReader(open(self.path, 'rb')) for page in range(1, pdf.getNumPages() + 1): out.append("/%04d.jpg" % (page)) return out + #------------------------------------------------------------------ class ComicArchive: @@ -603,8 +637,8 @@ class ComicArchive: class ArchiveType: Zip, Rar, Folder, Pdf, Unknown = range(5) - - def __init__( self, path, rar_exe_path=None, default_image_path=None ): + + def __init__(self, path, rar_exe_path=None, default_image_path=None): self.path = path self.rar_exe_path = rar_exe_path @@ -616,25 +650,27 @@ class ComicArchive: # Use file extension to decide which archive test we do first ext = os.path.splitext(path)[1].lower() - self.archive_type = self.ArchiveType.Unknown - self.archiver = UnknownArchiver( self.path ) + self.archive_type = self.ArchiveType.Unknown + self.archiver = UnknownArchiver(self.path) if ext == ".cbr" or ext == ".rar": if self.rarTest(): - self.archive_type = self.ArchiveType.Rar - self.archiver = RarArchiver( self.path, rar_exe_path=self.rar_exe_path ) + self.archive_type = self.ArchiveType.Rar + self.archiver = RarArchiver( + self.path, rar_exe_path=self.rar_exe_path) elif self.zipTest(): - self.archive_type = self.ArchiveType.Zip - self.archiver = ZipArchiver( self.path ) + self.archive_type = self.ArchiveType.Zip + self.archiver = ZipArchiver(self.path) else: if self.zipTest(): - self.archive_type = self.ArchiveType.Zip - self.archiver = ZipArchiver( self.path ) + self.archive_type = self.ArchiveType.Zip + self.archiver = ZipArchiver(self.path) elif self.rarTest(): - self.archive_type = self.ArchiveType.Rar - self.archiver = RarArchiver( self.path, rar_exe_path=self.rar_exe_path ) + self.archive_type = self.ArchiveType.Rar + self.archiver = RarArchiver( + self.path, rar_exe_path=self.rar_exe_path) elif os.path.basename(self.path)[-3:] == 'pdf': self.archive_type = self.ArchiveType.Pdf self.archiver = PdfArchiver(self.path) @@ -646,49 +682,50 @@ class ComicArchive: ComicArchive.logo_data = fd.read() # Clears the cached data - def resetCache( self ): + def resetCache(self): self.has_cix = None self.has_cbi = None self.has_comet = None self.comet_filename = None - self.page_count = None - self.page_list = None - self.cix_md = None - self.cbi_md = None - self.comet_md = None + self.page_count = None + self.page_list = None + self.cix_md = None + self.cbi_md = None + self.comet_md = None - def loadCache( self, style_list ): + def loadCache(self, style_list): for style in style_list: self.readMetadata(style) - def rename( self, path ): + def rename(self, path): self.path = path self.archiver.path = path - def zipTest( self ): - return zipfile.is_zipfile( self.path ) + def zipTest(self): + return zipfile.is_zipfile(self.path) - def rarTest( self ): + def rarTest(self): try: - rarc = rarfile.RarFile( self.path ) - except: # InvalidRARArchive: + rarc = rarfile.RarFile(self.path) + except: # InvalidRARArchive: return False else: return True + def isZip(self): + return self.archive_type == self.ArchiveType.Zip - def isZip( self ): - return self.archive_type == self.ArchiveType.Zip + def isRar(self): + return self.archive_type == self.ArchiveType.Rar - def isRar( self ): - return self.archive_type == self.ArchiveType.Rar def isPdf(self): - return self.archive_type == self.ArchiveType.Pdf - def isFolder( self ): - return self.archive_type == self.ArchiveType.Folder + return self.archive_type == self.ArchiveType.Pdf - def isWritable( self, check_rar_status=True ): - if self.archive_type == self.ArchiveType.Unknown : + def isFolder(self): + return self.archive_type == self.ArchiveType.Folder + + def isWritable(self, check_rar_status=True): + if self.archive_type == self.ArchiveType.Unknown: return False elif check_rar_status and self.isRar() and self.rar_exe_path is None: @@ -697,35 +734,33 @@ class ComicArchive: elif not os.access(self.path, os.W_OK): return False - elif ((self.archive_type != self.ArchiveType.Folder) and - (not os.access( os.path.dirname( os.path.abspath(self.path)), os.W_OK ))): + elif ((self.archive_type != self.ArchiveType.Folder) + and (not os.access( + os.path.dirname(os.path.abspath(self.path)), os.W_OK))): return False return True - def isWritableForStyle( self, data_style ): + def isWritableForStyle(self, data_style): if self.isRar() and data_style == MetaDataStyle.CBI: return False return self.isWritable() - def seemsToBeAComicArchive( self ): + def seemsToBeAComicArchive(self): # Do we even care about extensions?? ext = os.path.splitext(self.path)[1].lower() - if ( - ( self.isZip() or self.isRar() or self.isPdf()) #or self.isFolder() ) - and - ( self.getNumberOfPages() > 0) - - ): + if ((self.isZip() or self.isRar() or self.isPdf() + ) #or self.isFolder() ) + and (self.getNumberOfPages() > 0)): return True else: return False - def readMetadata( self, style ): + def readMetadata(self, style): if style == MetaDataStyle.CIX: return self.readCIX() @@ -736,19 +771,18 @@ class ComicArchive: else: return GenericMetadata() - def writeMetadata( self, metadata, style ): + def writeMetadata(self, metadata, style): retcode = None if style == MetaDataStyle.CIX: - retcode = self.writeCIX( metadata ) + retcode = self.writeCIX(metadata) elif style == MetaDataStyle.CBI: - retcode = self.writeCBI( metadata ) + retcode = self.writeCBI(metadata) elif style == MetaDataStyle.COMET: - retcode = self.writeCoMet( metadata ) + retcode = self.writeCoMet(metadata) return retcode - - def hasMetadata( self, style ): + def hasMetadata(self, style): if style == MetaDataStyle.CIX: return self.hasCIX() @@ -759,7 +793,7 @@ class ComicArchive: else: return False - def removeMetadata( self, style ): + def removeMetadata(self, style): retcode = True if style == MetaDataStyle.CIX: retcode = self.removeCIX() @@ -769,36 +803,36 @@ class ComicArchive: retcode = self.removeCoMet() return retcode - def getPage( self, index ): + def getPage(self, index): image_data = None - filename = self.getPageName( index ) + filename = self.getPageName(index) if filename is not None: try: - image_data = self.archiver.readArchiveFile( filename ) + image_data = self.archiver.readArchiveFile(filename) except IOError: - errMsg=u"Error reading in page. Substituting logo page." - sys.stderr.buffer.write(bytes(errMsg,"UTF-8")) + errMsg = u"Error reading in page. Substituting logo page." + sys.stderr.buffer.write(bytes(errMsg, "UTF-8")) image_data = ComicArchive.logo_data return image_data - def getPageName( self, index ): + def getPageName(self, index): if index is None: return None page_list = self.getPageNameList() - num_pages = len( page_list ) + num_pages = len(page_list) if num_pages == 0 or index >= num_pages: return None - return page_list[index] + return page_list[index] - def getScannerPageIndex( self ): + def getScannerPageIndex(self): scanner_page_index = None @@ -813,42 +847,42 @@ class ComicArchive: # count the length of every filename, and count occurences length_buckets = dict() for name in name_list: - fname = os.path.split(name)[1] + fname = os.path.split(name)[1] length = len(fname) if length in length_buckets: - length_buckets[ length ] += 1 + length_buckets[length] += 1 else: - length_buckets[ length ] = 1 + length_buckets[length] = 1 # sort by most common - sorted_buckets = sorted(length_buckets.items(), key=lambda k,v: (v,k), reverse=True) + sorted_buckets = sorted( + length_buckets.items(), key=lambda k, v: (v, k), reverse=True) # statistical mode occurence is first mode_length = sorted_buckets[0][0] # we are only going to consider the final image file: - final_name = os.path.split(name_list[count-1])[1] + final_name = os.path.split(name_list[count - 1])[1] common_length_list = list() for name in name_list: if len(os.path.split(name)[1]) == mode_length: - common_length_list.append( os.path.split(name)[1] ) + common_length_list.append(os.path.split(name)[1]) prefix = os.path.commonprefix(common_length_list) if mode_length <= 7 and prefix == "": #probably all numbers if len(final_name) > mode_length: - scanner_page_index = count-1 + scanner_page_index = count - 1 # see if the last page doesn't start with the same prefix as most others elif not final_name.startswith(prefix): - scanner_page_index = count-1 + scanner_page_index = count - 1 return scanner_page_index - - def getPageNameList( self , sort_list=True): + def getPageNameList(self, sort_list=True): if self.page_list is None: # get the list file names in the archive, and sort @@ -856,6 +890,7 @@ class ComicArchive: # seems like some archive creators are on Windows, and don't know about case-sensitivity! if sort_list: + def keyfunc(k): #hack to account for some weird scanner ID pages #basename=os.path.split(k)[1] @@ -863,36 +898,38 @@ class ComicArchive: # k = os.path.join(os.path.split(k)[0], "z" + basename) return k.lower() - files = natsorted(files, key=keyfunc,signed=False) + files = natsorted(files, key=keyfunc, signed=False) # make a sub-list of image files self.page_list = [] for name in files: - if ( name[-4:].lower() in [ ".jpg", "jpeg", ".png", ".gif", "webp" ] and os.path.basename(name)[0] != "." ): + if (name[-4:].lower() in [ + ".jpg", "jpeg", ".png", ".gif", "webp" + ] and os.path.basename(name)[0] != "."): self.page_list.append(name) return self.page_list - def getNumberOfPages( self ): + def getNumberOfPages(self): if self.page_count is None: - self.page_count = len( self.getPageNameList( ) ) + self.page_count = len(self.getPageNameList()) return self.page_count - def readCBI( self ): + def readCBI(self): if self.cbi_md is None: raw_cbi = self.readRawCBI() if raw_cbi is None: self.cbi_md = GenericMetadata() else: - self.cbi_md = ComicBookInfo().metadataFromString( raw_cbi ) + self.cbi_md = ComicBookInfo().metadataFromString(raw_cbi) - self.cbi_md.setDefaultPageList( self.getNumberOfPages() ) + self.cbi_md.setDefaultPageList(self.getNumberOfPages()) return self.cbi_md - def readRawCBI( self ): - if ( not self.hasCBI() ): + def readRawCBI(self): + if (not self.hasCBI()): return None return self.archiver.getArchiveComment() @@ -905,15 +942,15 @@ class ComicArchive: self.has_cbi = False else: comment = self.archiver.getArchiveComment() - self.has_cbi = ComicBookInfo().validateString( comment ) + self.has_cbi = ComicBookInfo().validateString(comment) return self.has_cbi - def writeCBI( self, metadata ): + def writeCBI(self, metadata): if metadata is not None: - self.applyArchiveInfoToMetadata( metadata ) - cbi_string = ComicBookInfo().stringFromMetadata( metadata ) - write_success = self.archiver.setArchiveComment( cbi_string ) + self.applyArchiveInfoToMetadata(metadata) + cbi_string = ComicBookInfo().stringFromMetadata(metadata) + write_success = self.archiver.setArchiveComment(cbi_string) if write_success: self.has_cbi = True self.cbi_md = metadata @@ -922,9 +959,9 @@ class ComicArchive: else: return False - def removeCBI( self ): + def removeCBI(self): if self.hasCBI(): - write_success = self.archiver.setArchiveComment( "" ) + write_success = self.archiver.setArchiveComment("") if write_success: self.has_cbi = False self.cbi_md = None @@ -932,42 +969,43 @@ class ComicArchive: return write_success return True - def readCIX( self ): + def readCIX(self): if self.cix_md is None: raw_cix = self.readRawCIX() if raw_cix is None or raw_cix == "": self.cix_md = GenericMetadata() else: - self.cix_md = ComicInfoXml().metadataFromString( raw_cix ) + self.cix_md = ComicInfoXml().metadataFromString(raw_cix) #validate the existing page list (make sure count is correct) - if len ( self.cix_md.pages ) != 0 : - if len ( self.cix_md.pages ) != self.getNumberOfPages(): + if len(self.cix_md.pages) != 0: + if len(self.cix_md.pages) != self.getNumberOfPages(): # pages array doesn't match the actual number of images we're seeing # in the archive, so discard the data self.cix_md.pages = [] - if len( self.cix_md.pages ) == 0: - self.cix_md.setDefaultPageList( self.getNumberOfPages() ) + if len(self.cix_md.pages) == 0: + self.cix_md.setDefaultPageList(self.getNumberOfPages()) return self.cix_md - def readRawCIX( self ): + def readRawCIX(self): if not self.hasCIX(): return None try: - raw_cix = self.archiver.readArchiveFile( self.ci_xml_filename ) + raw_cix = self.archiver.readArchiveFile(self.ci_xml_filename) except IOError: - print ("Error reading in raw CIX!") + print("Error reading in raw CIX!") raw_cix = "" - return raw_cix + return raw_cix def writeCIX(self, metadata): if metadata is not None: - self.applyArchiveInfoToMetadata( metadata, calc_page_sizes=True ) - cix_string = ComicInfoXml().stringFromMetadata( metadata ) - write_success = self.archiver.writeArchiveFile( self.ci_xml_filename, cix_string ) + self.applyArchiveInfoToMetadata(metadata, calc_page_sizes=True) + cix_string = ComicInfoXml().stringFromMetadata(metadata) + write_success = self.archiver.writeArchiveFile( + self.ci_xml_filename, cix_string) if write_success: self.has_cix = True self.cix_md = metadata @@ -976,9 +1014,10 @@ class ComicArchive: else: return False - def removeCIX( self ): + def removeCIX(self): if self.hasCIX(): - write_success = self.archiver.removeArchiveFile( self.ci_xml_filename ) + write_success = self.archiver.removeArchiveFile( + self.ci_xml_filename) if write_success: self.has_cix = False self.cix_md = None @@ -986,56 +1025,56 @@ class ComicArchive: return write_success return True - def hasCIX(self): if self.has_cix is None: if not self.seemsToBeAComicArchive(): self.has_cix = False - elif self.ci_xml_filename in self.archiver.getArchiveFilenameList(): + elif self.ci_xml_filename in self.archiver.getArchiveFilenameList( + ): self.has_cix = True else: self.has_cix = False return self.has_cix - - def readCoMet( self ): + def readCoMet(self): if self.comet_md is None: raw_comet = self.readRawCoMet() if raw_comet is None or raw_comet == "": self.comet_md = GenericMetadata() else: - self.comet_md = CoMet().metadataFromString( raw_comet ) + self.comet_md = CoMet().metadataFromString(raw_comet) - self.comet_md.setDefaultPageList( self.getNumberOfPages() ) + self.comet_md.setDefaultPageList(self.getNumberOfPages()) #use the coverImage value from the comet_data to mark the cover in this struct # walk through list of images in file, and find the matching one for md.coverImage # need to remove the existing one in the default if self.comet_md.coverImage is not None: cover_idx = 0 - for idx,f in enumerate(self.getPageNameList()): + for idx, f in enumerate(self.getPageNameList()): if self.comet_md.coverImage == f: cover_idx = idx break if cover_idx != 0: - del (self.comet_md.pages[0]['Type'] ) - self.comet_md.pages[ cover_idx ]['Type'] = PageType.FrontCover + del (self.comet_md.pages[0]['Type']) + self.comet_md.pages[cover_idx][ + 'Type'] = PageType.FrontCover return self.comet_md - def readRawCoMet( self ): + def readRawCoMet(self): if not self.hasCoMet(): - errMsg=u"{} doesn't have CoMet data!".format(self.path) - sys.stderr.buffer.write(bytes(errMsg,"UTF-8")) + errMsg = u"{} doesn't have CoMet data!".format(self.path) + sys.stderr.buffer.write(bytes(errMsg, "UTF-8")) return None try: - raw_comet = self.archiver.readArchiveFile( self.comet_filename ) + raw_comet = self.archiver.readArchiveFile(self.comet_filename) except IOError: - errMsg=u"Error reading in raw CoMet!" - sys.stderr.buffer.write(bytes(errMsg,"UTF-8")) + errMsg = u"Error reading in raw CoMet!" + sys.stderr.buffer.write(bytes(errMsg, "UTF-8")) raw_comet = "" - return raw_comet + return raw_comet def writeCoMet(self, metadata): @@ -1043,14 +1082,15 @@ class ComicArchive: if not self.hasCoMet(): self.comet_filename = self.comet_default_filename - self.applyArchiveInfoToMetadata( metadata ) + self.applyArchiveInfoToMetadata(metadata) # Set the coverImage value, if it's not the first page cover_idx = int(metadata.getCoverPageIndexList()[0]) if cover_idx != 0: - metadata.coverImage = self.getPageName( cover_idx ) + metadata.coverImage = self.getPageName(cover_idx) - comet_string = CoMet().stringFromMetadata( metadata ) - write_success = self.archiver.writeArchiveFile( self.comet_filename, comet_string ) + comet_string = CoMet().stringFromMetadata(metadata) + write_success = self.archiver.writeArchiveFile( + self.comet_filename, comet_string) if write_success: self.has_comet = True self.comet_md = metadata @@ -1059,9 +1099,10 @@ class ComicArchive: else: return False - def removeCoMet( self ): + def removeCoMet(self): if self.hasCoMet(): - write_success = self.archiver.removeArchiveFile( self.comet_filename ) + write_success = self.archiver.removeArchiveFile( + self.comet_filename) if write_success: self.has_comet = False self.comet_md = None @@ -1077,16 +1118,16 @@ class ComicArchive: #look at all xml files in root, and search for CoMet data, get first for n in self.archiver.getArchiveFilenameList(): - if ( os.path.dirname(n) == "" and - os.path.splitext(n)[1].lower() == '.xml'): + if (os.path.dirname(n) == "" + and os.path.splitext(n)[1].lower() == '.xml'): # read in XML file, and validate it try: - data = self.archiver.readArchiveFile( n ) + data = self.archiver.readArchiveFile(n) except: data = "" - errMsg=u"Error reading in Comet XML for validation!" - sys.stderr.buffer.write(bytes(errMsg,"UTF-8")) - if CoMet().validateString( data ): + errMsg = u"Error reading in Comet XML for validation!" + sys.stderr.buffer.write(bytes(errMsg, "UTF-8")) + if CoMet().validateString(data): # since we found it, save it! self.comet_filename = n self.has_comet = True @@ -1094,21 +1135,19 @@ class ComicArchive: return self.has_comet - - - def applyArchiveInfoToMetadata( self, md, calc_page_sizes=False): + def applyArchiveInfoToMetadata(self, md, calc_page_sizes=False): md.pageCount = self.getNumberOfPages() if calc_page_sizes: for p in md.pages: - idx = int( p['Image'] ) + idx = int(p['Image']) if pil_available: if 'ImageSize' not in p or 'ImageHeight' not in p or 'ImageWidth' not in p: - data = self.getPage( idx ) + data = self.getPage(idx) if data is not None: try: im = Image.open(StringIO(data)) - w,h = im.size + w, h = im.size p['ImageSize'] = str(len(data)) p['ImageHeight'] = str(h) @@ -1118,17 +1157,15 @@ class ComicArchive: else: if 'ImageSize' not in p: - data = self.getPage( idx ) + data = self.getPage(idx) p['ImageSize'] = str(len(data)) - - - def metadataFromFilename( self , parse_scan_info=True): + def metadataFromFilename(self, parse_scan_info=True): metadata = GenericMetadata() fnp = FileNameParser() - fnp.parseFilename( self.path ) + fnp.parseFilename(self.path) if fnp.issue != "": metadata.issue = fnp.issue @@ -1148,11 +1185,10 @@ class ComicArchive: return metadata - def exportAsZip( self, zipfilename ): + def exportAsZip(self, zipfilename): if self.archive_type == self.ArchiveType.Zip: # nothing to do, we're already a zip return True - zip_archiver = ZipArchiver( zipfilename ) - return zip_archiver.copyFromArchive( self.archiver ) - + zip_archiver = ZipArchiver(zipfilename) + return zip_archiver.copyFromArchive(self.archiver) diff --git a/comicbookinfo.py b/comicbookinfo.py old mode 100644 new mode 100755 index e1edaba..2076b9a --- a/comicbookinfo.py +++ b/comicbookinfo.py @@ -1,8 +1,6 @@ """ A python class to encapsulate the ComicBookInfo data -""" -""" Copyright 2012-2014 Anthony Beville Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,135 +16,135 @@ See the License for the specific language governing permissions and limitations under the License. """ - import json from datetime import datetime import zipfile from comicapi.genericmetadata import GenericMetadata import comicapi.utils + #import ctversion + class ComicBookInfo: - + def metadataFromString(self, string): - def metadataFromString( self, string ): - - cbi_container = json.loads( str(string, 'utf-8') ) + cbi_container = json.loads(str(string, 'utf-8')) - metadata = GenericMetadata() + metadata = GenericMetadata() - cbi = cbi_container[ 'ComicBookInfo/1.0' ] + cbi = cbi_container['ComicBookInfo/1.0'] - #helper func - # If item is not in CBI, return None - def xlate( cbi_entry): - if cbi_entry in cbi: - return cbi[cbi_entry] - else: - return None - - metadata.series = xlate( 'series' ) - metadata.title = xlate( 'title' ) - metadata.issue = xlate( 'issue' ) - metadata.publisher = xlate( 'publisher' ) - metadata.month = xlate( 'publicationMonth' ) - metadata.year = xlate( 'publicationYear' ) - metadata.issueCount = xlate( 'numberOfIssues' ) - metadata.comments = xlate( 'comments' ) - metadata.credits = xlate( 'credits' ) - metadata.genre = xlate( 'genre' ) - metadata.volume = xlate( 'volume' ) - metadata.volumeCount = xlate( 'numberOfVolumes' ) - metadata.language = xlate( 'language' ) - metadata.country = xlate( 'country' ) - metadata.criticalRating = xlate( 'rating' ) - metadata.tags = xlate( 'tags' ) - - # make sure credits and tags are at least empty lists and not None - if metadata.credits is None: - metadata.credits = [] - if metadata.tags is None: - metadata.tags = [] - - #need to massage the language string to be ISO - if metadata.language is not None: - # reverse look-up - pattern = metadata.language - metadata.language = None - for key in comicapi.utils.getLanguageDict(): - if comicapi.utils.getLanguageDict()[ key ] == pattern.encode('utf-8'): - metadata.language = key - break - - metadata.isEmpty = False - - return metadata + #helper func + # If item is not in CBI, return None + def xlate(cbi_entry): + if cbi_entry in cbi: + return cbi[cbi_entry] + else: + return None - def stringFromMetadata( self, metadata ): + metadata.series = xlate('series') + metadata.title = xlate('title') + metadata.issue = xlate('issue') + metadata.publisher = xlate('publisher') + metadata.month = xlate('publicationMonth') + metadata.year = xlate('publicationYear') + metadata.issueCount = xlate('numberOfIssues') + metadata.comments = xlate('comments') + metadata.credits = xlate('credits') + metadata.genre = xlate('genre') + metadata.volume = xlate('volume') + metadata.volumeCount = xlate('numberOfVolumes') + metadata.language = xlate('language') + metadata.country = xlate('country') + metadata.criticalRating = xlate('rating') + metadata.tags = xlate('tags') - cbi_container = self.createJSONDictionary( metadata ) - return json.dumps( cbi_container ) - - #verify that the string actually contains CBI data in JSON format - def validateString( self, string ): - - try: - cbi_container = json.loads( string ) - except: - return False - - return ( 'ComicBookInfo/1.0' in cbi_container ) + # make sure credits and tags are at least empty lists and not None + if metadata.credits is None: + metadata.credits = [] + if metadata.tags is None: + metadata.tags = [] + #need to massage the language string to be ISO + if metadata.language is not None: + # reverse look-up + pattern = metadata.language + metadata.language = None + for key in comicapi.utils.getLanguageDict(): + if comicapi.utils.getLanguageDict()[key] == pattern.encode( + 'utf-8'): + metadata.language = key + break - def createJSONDictionary( self, metadata ): - - # Create the dictionary that we will convert to JSON text - cbi = dict() - cbi_container = {'appID' : 'ComicTagger/' + '1.0.0', #ctversion.version, - 'lastModified' : str(datetime.now()), - 'ComicBookInfo/1.0' : cbi } - - #helper func - def assign( cbi_entry, md_entry): - if md_entry is not None: - cbi[cbi_entry] = md_entry - - #helper func - def toInt(s): - i = None - if type(s) in [ str, int ]: - try: - i = int(s) - except ValueError: - pass - return i - - assign( 'series', metadata.series ) - assign( 'title', metadata.title ) - assign( 'issue', metadata.issue ) - assign( 'publisher', metadata.publisher ) - assign( 'publicationMonth', toInt(metadata.month) ) - assign( 'publicationYear', toInt(metadata.year) ) - assign( 'numberOfIssues', toInt(metadata.issueCount) ) - assign( 'comments', metadata.comments ) - assign( 'genre', metadata.genre ) - assign( 'volume', toInt(metadata.volume) ) - assign( 'numberOfVolumes', toInt(metadata.volumeCount) ) - assign( 'language', comicapi.utils.getLanguageFromISO(metadata.language) ) - assign( 'country', metadata.country ) - assign( 'rating', metadata.criticalRating ) - assign( 'credits', metadata.credits ) - assign( 'tags', metadata.tags ) - - return cbi_container - + metadata.isEmpty = False - def writeToExternalFile( self, filename, metadata ): + return metadata - cbi_container = self.createJSONDictionary(metadata) + def stringFromMetadata(self, metadata): - f = open(filename, 'w') - f.write(json.dumps(cbi_container, indent=4)) - f.close + cbi_container = self.createJSONDictionary(metadata) + return json.dumps(cbi_container) + #verify that the string actually contains CBI data in JSON format + def validateString(self, string): + + try: + cbi_container = json.loads(string) + except: + return False + + return ('ComicBookInfo/1.0' in cbi_container) + + def createJSONDictionary(self, metadata): + + # Create the dictionary that we will convert to JSON text + cbi = dict() + cbi_container = { + 'appID': 'ComicTagger/' + '1.0.0', #ctversion.version, + 'lastModified': str(datetime.now()), + 'ComicBookInfo/1.0': cbi + } + + #helper func + def assign(cbi_entry, md_entry): + if md_entry is not None: + cbi[cbi_entry] = md_entry + + #helper func + def toInt(s): + i = None + if type(s) in [str, int]: + try: + i = int(s) + except ValueError: + pass + return i + + assign('series', metadata.series) + assign('title', metadata.title) + assign('issue', metadata.issue) + assign('publisher', metadata.publisher) + assign('publicationMonth', toInt(metadata.month)) + assign('publicationYear', toInt(metadata.year)) + assign('numberOfIssues', toInt(metadata.issueCount)) + assign('comments', metadata.comments) + assign('genre', metadata.genre) + assign('volume', toInt(metadata.volume)) + assign('numberOfVolumes', toInt(metadata.volumeCount)) + assign('language', comicapi.utils.getLanguageFromISO( + metadata.language)) + assign('country', metadata.country) + assign('rating', metadata.criticalRating) + assign('credits', metadata.credits) + assign('tags', metadata.tags) + + return cbi_container + + def writeToExternalFile(self, filename, metadata): + + cbi_container = self.createJSONDictionary(metadata) + + f = open(filename, 'w') + f.write(json.dumps(cbi_container, indent=4)) + f.close diff --git a/comicinfoxml.py b/comicinfoxml.py old mode 100644 new mode 100755 index 6301782..3d6f4ce --- a/comicinfoxml.py +++ b/comicinfoxml.py @@ -1,8 +1,6 @@ """ A python class to encapsulate ComicRack's ComicInfo.xml data -""" -""" Copyright 2012-2014 Anthony Beville Licensed under the Apache License, Version 2.0 (the "License"); @@ -20,274 +18,268 @@ limitations under the License. from datetime import datetime import zipfile -from pprint import pprint +from pprint import pprint import xml.etree.ElementTree as ET from comicapi.genericmetadata import GenericMetadata import comicapi.utils + class ComicInfoXml: - - writer_synonyms = ['writer', 'plotter', 'scripter'] - penciller_synonyms = [ 'artist', 'penciller', 'penciler', 'breakdowns' ] - inker_synonyms = [ 'inker', 'artist', 'finishes' ] - colorist_synonyms = [ 'colorist', 'colourist', 'colorer', 'colourer' ] - letterer_synonyms = [ 'letterer'] - cover_synonyms = [ 'cover', 'covers', 'coverartist', 'cover artist' ] - editor_synonyms = [ 'editor'] + writer_synonyms = ['writer', 'plotter', 'scripter'] + penciller_synonyms = ['artist', 'penciller', 'penciler', 'breakdowns'] + inker_synonyms = ['inker', 'artist', 'finishes'] + colorist_synonyms = ['colorist', 'colourist', 'colorer', 'colourer'] + letterer_synonyms = ['letterer'] + cover_synonyms = ['cover', 'covers', 'coverartist', 'cover artist'] + editor_synonyms = ['editor'] - def getParseableCredits( self ): - parsable_credits = [] - parsable_credits.extend( self.writer_synonyms ) - parsable_credits.extend( self.penciller_synonyms ) - parsable_credits.extend( self.inker_synonyms ) - parsable_credits.extend( self.colorist_synonyms ) - parsable_credits.extend( self.letterer_synonyms ) - parsable_credits.extend( self.cover_synonyms ) - parsable_credits.extend( self.editor_synonyms ) - return parsable_credits - - def metadataFromString( self, string ): + def getParseableCredits(self): + parsable_credits = [] + parsable_credits.extend(self.writer_synonyms) + parsable_credits.extend(self.penciller_synonyms) + parsable_credits.extend(self.inker_synonyms) + parsable_credits.extend(self.colorist_synonyms) + parsable_credits.extend(self.letterer_synonyms) + parsable_credits.extend(self.cover_synonyms) + parsable_credits.extend(self.editor_synonyms) + return parsable_credits - tree = ET.ElementTree(ET.fromstring( string )) - return self.convertXMLToMetadata( tree ) + def metadataFromString(self, string): - def stringFromMetadata( self, metadata ): + tree = ET.ElementTree(ET.fromstring(string)) + return self.convertXMLToMetadata(tree) - header = '\n' - - tree = self.convertMetadataToXML( self, metadata ) - return header + ET.tostring(tree.getroot()) + def stringFromMetadata(self, metadata): - def indent( self, elem, level=0 ): - # for making the XML output readable - i = "\n" + level*" " - if len(elem): - if not elem.text or not elem.text.strip(): - elem.text = i + " " - if not elem.tail or not elem.tail.strip(): - elem.tail = i - for elem in elem: - self.indent( elem, level+1 ) - if not elem.tail or not elem.tail.strip(): - elem.tail = i - else: - if level and (not elem.tail or not elem.tail.strip()): - elem.tail = i - - def convertMetadataToXML( self, filename, metadata ): + header = '\n' - #shorthand for the metadata - md = metadata + tree = self.convertMetadataToXML(self, metadata) + return header + ET.tostring(tree.getroot()) - # build a tree structure - root = ET.Element("ComicInfo") - root.attrib['xmlns:xsi']="http://www.w3.org/2001/XMLSchema-instance" - root.attrib['xmlns:xsd']="http://www.w3.org/2001/XMLSchema" - #helper func - def assign( cix_entry, md_entry): - if md_entry is not None: - ET.SubElement(root, cix_entry).text = u"{0}".format(md_entry) + def indent(self, elem, level=0): + # for making the XML output readable + i = "\n" + level * " " + if len(elem): + if not elem.text or not elem.text.strip(): + elem.text = i + " " + if not elem.tail or not elem.tail.strip(): + elem.tail = i + for elem in elem: + self.indent(elem, level + 1) + if not elem.tail or not elem.tail.strip(): + elem.tail = i + else: + if level and (not elem.tail or not elem.tail.strip()): + elem.tail = i - assign( 'Title', md.title ) - assign( 'Series', md.series ) - assign( 'Number', md.issue ) - assign( 'Count', md.issueCount ) - assign( 'Volume', md.volume ) - assign( 'AlternateSeries', md.alternateSeries ) - assign( 'AlternateNumber', md.alternateNumber ) - assign( 'StoryArc', md.storyArc ) - assign( 'SeriesGroup', md.seriesGroup ) - assign( 'AlternateCount', md.alternateCount ) - assign( 'Summary', md.comments ) - assign( 'Notes', md.notes ) - assign( 'Year', md.year ) - assign( 'Month', md.month ) - assign( 'Day', md.day ) + def convertMetadataToXML(self, filename, metadata): - # need to specially process the credits, since they are structured differently than CIX - credit_writer_list = list() - credit_penciller_list = list() - credit_inker_list = list() - credit_colorist_list = list() - credit_letterer_list = list() - credit_cover_list = list() - credit_editor_list = list() - - # first, loop thru credits, and build a list for each role that CIX supports - for credit in metadata.credits: + #shorthand for the metadata + md = metadata - if credit['role'].lower() in set( self.writer_synonyms ): - credit_writer_list.append(credit['person'].replace(",","")) + # build a tree structure + root = ET.Element("ComicInfo") + root.attrib['xmlns:xsi'] = "http://www.w3.org/2001/XMLSchema-instance" + root.attrib['xmlns:xsd'] = "http://www.w3.org/2001/XMLSchema" - if credit['role'].lower() in set( self.penciller_synonyms ): - credit_penciller_list.append(credit['person'].replace(",","")) - - if credit['role'].lower() in set( self.inker_synonyms ): - credit_inker_list.append(credit['person'].replace(",","")) - - if credit['role'].lower() in set( self.colorist_synonyms ): - credit_colorist_list.append(credit['person'].replace(",","")) + #helper func + def assign(cix_entry, md_entry): + if md_entry is not None: + ET.SubElement(root, cix_entry).text = u"{0}".format(md_entry) - if credit['role'].lower() in set( self.letterer_synonyms ): - credit_letterer_list.append(credit['person'].replace(",","")) + assign('Title', md.title) + assign('Series', md.series) + assign('Number', md.issue) + assign('Count', md.issueCount) + assign('Volume', md.volume) + assign('AlternateSeries', md.alternateSeries) + assign('AlternateNumber', md.alternateNumber) + assign('StoryArc', md.storyArc) + assign('SeriesGroup', md.seriesGroup) + assign('AlternateCount', md.alternateCount) + assign('Summary', md.comments) + assign('Notes', md.notes) + assign('Year', md.year) + assign('Month', md.month) + assign('Day', md.day) - if credit['role'].lower() in set( self.cover_synonyms ): - credit_cover_list.append(credit['person'].replace(",","")) + # need to specially process the credits, since they are structured differently than CIX + credit_writer_list = list() + credit_penciller_list = list() + credit_inker_list = list() + credit_colorist_list = list() + credit_letterer_list = list() + credit_cover_list = list() + credit_editor_list = list() - if credit['role'].lower() in set( self.editor_synonyms ): - credit_editor_list.append(credit['person'].replace(",","")) - - # second, convert each list to string, and add to XML struct - if len( credit_writer_list ) > 0: - node = ET.SubElement(root, 'Writer') - node.text = comicapi.utils.listToString( credit_writer_list ) + # first, loop thru credits, and build a list for each role that CIX supports + for credit in metadata.credits: - if len( credit_penciller_list ) > 0: - node = ET.SubElement(root, 'Penciller') - node.text = comicapi.utils.listToString( credit_penciller_list ) + if credit['role'].lower() in set(self.writer_synonyms): + credit_writer_list.append(credit['person'].replace(",", "")) - if len( credit_inker_list ) > 0: - node = ET.SubElement(root, 'Inker') - node.text = comicapi.utils.listToString( credit_inker_list ) + if credit['role'].lower() in set(self.penciller_synonyms): + credit_penciller_list.append(credit['person'].replace(",", "")) - if len( credit_colorist_list ) > 0: - node = ET.SubElement(root, 'Colorist') - node.text = comicapi.utils.listToString( credit_colorist_list ) + if credit['role'].lower() in set(self.inker_synonyms): + credit_inker_list.append(credit['person'].replace(",", "")) - if len( credit_letterer_list ) > 0: - node = ET.SubElement(root, 'Letterer') - node.text = comicapi.utils.listToString( credit_letterer_list ) + if credit['role'].lower() in set(self.colorist_synonyms): + credit_colorist_list.append(credit['person'].replace(",", "")) - if len( credit_cover_list ) > 0: - node = ET.SubElement(root, 'CoverArtist') - node.text = comicapi.utils.listToString( credit_cover_list ) - - if len( credit_editor_list ) > 0: - node = ET.SubElement(root, 'Editor') - node.text = comicapi.utils.listToString( credit_editor_list ) + if credit['role'].lower() in set(self.letterer_synonyms): + credit_letterer_list.append(credit['person'].replace(",", "")) - assign( 'Publisher', md.publisher ) - assign( 'Imprint', md.imprint ) - assign( 'Genre', md.genre ) - assign( 'Web', md.webLink ) - assign( 'PageCount', md.pageCount ) - assign( 'LanguageISO', md.language ) - assign( 'Format', md.format ) - assign( 'AgeRating', md.maturityRating ) - if md.blackAndWhite is not None and md.blackAndWhite: - ET.SubElement(root, 'BlackAndWhite').text = "Yes" - assign( 'Manga', md.manga ) - assign( 'Characters', md.characters ) - assign( 'Teams', md.teams ) - assign( 'Locations', md.locations ) - assign( 'ScanInformation', md.scanInfo ) + if credit['role'].lower() in set(self.cover_synonyms): + credit_cover_list.append(credit['person'].replace(",", "")) - # loop and add the page entries under pages node - if len( md.pages ) > 0: - pages_node = ET.SubElement(root, 'Pages') - for page_dict in md.pages: - page_node = ET.SubElement(pages_node, 'Page') - page_node.attrib = page_dict + if credit['role'].lower() in set(self.editor_synonyms): + credit_editor_list.append(credit['person'].replace(",", "")) - # self pretty-print - self.indent(root) + # second, convert each list to string, and add to XML struct + if len(credit_writer_list) > 0: + node = ET.SubElement(root, 'Writer') + node.text = comicapi.utils.listToString(credit_writer_list) - # wrap it in an ElementTree instance, and save as XML - tree = ET.ElementTree(root) - return tree - + if len(credit_penciller_list) > 0: + node = ET.SubElement(root, 'Penciller') + node.text = comicapi.utils.listToString(credit_penciller_list) - def convertXMLToMetadata( self, tree ): - - root = tree.getroot() + if len(credit_inker_list) > 0: + node = ET.SubElement(root, 'Inker') + node.text = comicapi.utils.listToString(credit_inker_list) - if root.tag != 'ComicInfo': - raise 1 - return None + if len(credit_colorist_list) > 0: + node = ET.SubElement(root, 'Colorist') + node.text = comicapi.utils.listToString(credit_colorist_list) - metadata = GenericMetadata() - md = metadata - - - # Helper function - def xlate( tag ): - node = root.find( tag ) - if node is not None: - return node.text - else: - return None - - md.series = xlate( 'Series' ) - md.title = xlate( 'Title' ) - md.issue = xlate( 'Number' ) - md.issueCount = xlate( 'Count' ) - md.volume = xlate( 'Volume' ) - md.alternateSeries = xlate( 'AlternateSeries' ) - md.alternateNumber = xlate( 'AlternateNumber' ) - md.alternateCount = xlate( 'AlternateCount' ) - md.comments = xlate( 'Summary' ) - md.notes = xlate( 'Notes' ) - md.year = xlate( 'Year' ) - md.month = xlate( 'Month' ) - md.day = xlate( 'Day' ) - md.publisher = xlate( 'Publisher' ) - md.imprint = xlate( 'Imprint' ) - md.genre = xlate( 'Genre' ) - md.webLink = xlate( 'Web' ) - md.language = xlate( 'LanguageISO' ) - md.format = xlate( 'Format' ) - md.manga = xlate( 'Manga' ) - md.characters = xlate( 'Characters' ) - md.teams = xlate( 'Teams' ) - md.locations = xlate( 'Locations' ) - md.pageCount = xlate( 'PageCount' ) - md.scanInfo = xlate( 'ScanInformation' ) - md.storyArc = xlate( 'StoryArc' ) - md.seriesGroup = xlate( 'SeriesGroup' ) - md.maturityRating = xlate( 'AgeRating' ) + if len(credit_letterer_list) > 0: + node = ET.SubElement(root, 'Letterer') + node.text = comicapi.utils.listToString(credit_letterer_list) - tmp = xlate( 'BlackAndWhite' ) - md.blackAndWhite = False - if tmp is not None and tmp.lower() in [ "yes", "true", "1" ]: - md.blackAndWhite = True - # Now extract the credit info - for n in root: - if ( n.tag == 'Writer' or - n.tag == 'Penciller' or - n.tag == 'Inker' or - n.tag == 'Colorist' or - n.tag == 'Letterer' or - n.tag == 'Editor' - ): - if n.text is not None: - for name in n.text.split(','): - metadata.addCredit( name.strip(), n.tag ) + if len(credit_cover_list) > 0: + node = ET.SubElement(root, 'CoverArtist') + node.text = comicapi.utils.listToString(credit_cover_list) - if n.tag == 'CoverArtist': - if n.text is not None: - for name in n.text.split(','): - metadata.addCredit( name.strip(), "Cover" ) + if len(credit_editor_list) > 0: + node = ET.SubElement(root, 'Editor') + node.text = comicapi.utils.listToString(credit_editor_list) - # parse page data now - pages_node = root.find( "Pages" ) - if pages_node is not None: - for page in pages_node: - metadata.pages.append( page.attrib ) - #print page.attrib + assign('Publisher', md.publisher) + assign('Imprint', md.imprint) + assign('Genre', md.genre) + assign('Web', md.webLink) + assign('PageCount', md.pageCount) + assign('LanguageISO', md.language) + assign('Format', md.format) + assign('AgeRating', md.maturityRating) + if md.blackAndWhite is not None and md.blackAndWhite: + ET.SubElement(root, 'BlackAndWhite').text = "Yes" + assign('Manga', md.manga) + assign('Characters', md.characters) + assign('Teams', md.teams) + assign('Locations', md.locations) + assign('ScanInformation', md.scanInfo) - metadata.isEmpty = False - - return metadata + # loop and add the page entries under pages node + if len(md.pages) > 0: + pages_node = ET.SubElement(root, 'Pages') + for page_dict in md.pages: + page_node = ET.SubElement(pages_node, 'Page') + page_node.attrib = page_dict - def writeToExternalFile( self, filename, metadata ): - - tree = self.convertMetadataToXML( self, metadata ) - #ET.dump(tree) - tree.write(filename, encoding='utf-8') - - def readFromExternalFile( self, filename ): + # self pretty-print + self.indent(root) - tree = ET.parse( filename ) - return self.convertXMLToMetadata( tree ) + # wrap it in an ElementTree instance, and save as XML + tree = ET.ElementTree(root) + return tree + def convertXMLToMetadata(self, tree): + + root = tree.getroot() + + if root.tag != 'ComicInfo': + raise KeyError("Not a ComicInfo XML!") + # return None + + metadata = GenericMetadata() + md = metadata + + # Helper function + def xlate(tag): + node = root.find(tag) + if node is not None: + return node.text + else: + return None + + md.series = xlate('Series') + md.title = xlate('Title') + md.issue = xlate('Number') + md.issueCount = xlate('Count') + md.volume = xlate('Volume') + md.alternateSeries = xlate('AlternateSeries') + md.alternateNumber = xlate('AlternateNumber') + md.alternateCount = xlate('AlternateCount') + md.comments = xlate('Summary') + md.notes = xlate('Notes') + md.year = xlate('Year') + md.month = xlate('Month') + md.day = xlate('Day') + md.publisher = xlate('Publisher') + md.imprint = xlate('Imprint') + md.genre = xlate('Genre') + md.webLink = xlate('Web') + md.language = xlate('LanguageISO') + md.format = xlate('Format') + md.manga = xlate('Manga') + md.characters = xlate('Characters') + md.teams = xlate('Teams') + md.locations = xlate('Locations') + md.pageCount = xlate('PageCount') + md.scanInfo = xlate('ScanInformation') + md.storyArc = xlate('StoryArc') + md.seriesGroup = xlate('SeriesGroup') + md.maturityRating = xlate('AgeRating') + + tmp = xlate('BlackAndWhite') + md.blackAndWhite = False + if tmp is not None and tmp.lower() in ["yes", "true", "1"]: + md.blackAndWhite = True + # Now extract the credit info + for n in root: + if (n.tag == 'Writer' or n.tag == 'Penciller' or n.tag == 'Inker' + or n.tag == 'Colorist' or n.tag == 'Letterer' + or n.tag == 'Editor'): + if n.text is not None: + for name in n.text.split(','): + metadata.addCredit(name.strip(), n.tag) + + if n.tag == 'CoverArtist': + if n.text is not None: + for name in n.text.split(','): + metadata.addCredit(name.strip(), "Cover") + + # parse page data now + pages_node = root.find("Pages") + if pages_node is not None: + for page in pages_node: + metadata.pages.append(page.attrib) + #print page.attrib + + metadata.isEmpty = False + + return metadata + + def writeToExternalFile(self, filename, metadata): + + tree = self.convertMetadataToXML(self, metadata) + #ET.dump(tree) + tree.write(filename, encoding='utf-8') + + def readFromExternalFile(self, filename): + + tree = ET.parse(filename) + return self.convertXMLToMetadata(tree) diff --git a/filenameparser.py b/filenameparser.py old mode 100644 new mode 100755 index 052a99b..d672c6b --- a/filenameparser.py +++ b/filenameparser.py @@ -3,9 +3,6 @@ Functions for parsing comic info from filename This should probably be re-written, but, well, it mostly works! -""" - -""" Copyright 2012-2014 Anthony Beville Licensed under the Apache License, Version 2.0 (the "License"); @@ -21,7 +18,6 @@ See the License for the specific language governing permissions and limitations under the License. """ - # Some portions of this code were modified from pyComicMetaThis project # http://code.google.com/p/pycomicmetathis/ @@ -29,249 +25,248 @@ import re import os from urllib.parse import unquote + class FileNameParser: + def repl(self, m): + return ' ' * len(m.group()) - def repl(self, m): - return ' ' * len(m.group()) - - def fixSpaces( self, string, remove_dashes=True ): - if remove_dashes: - placeholders = ['[-_]',' +'] - else: - placeholders = ['[_]',' +'] - for ph in placeholders: - string = re.sub(ph, self.repl, string ) - return string #.strip() + def fixSpaces(self, string, remove_dashes=True): + if remove_dashes: + placeholders = ['[-_]', ' +'] + else: + placeholders = ['[_]', ' +'] + for ph in placeholders: + string = re.sub(ph, self.repl, string) + return string #.strip() + def getIssueCount(self, filename, issue_end): - def getIssueCount( self,filename, issue_end ): + count = "" + filename = filename[issue_end:] - count = "" - filename = filename[issue_end:] - - # replace any name seperators with spaces - tmpstr = self.fixSpaces(filename) - found = False - - match = re.search('(?<=\sof\s)\d+(?=\s)', tmpstr, re.IGNORECASE) - if match: - count = match.group() - found = True + # replace any name seperators with spaces + tmpstr = self.fixSpaces(filename) + found = False - if not found: - match = re.search('(?<=\(of\s)\d+(?=\))', tmpstr, re.IGNORECASE) - if match: - count = match.group() - found = True - + match = re.search('(?<=\sof\s)\d+(?=\s)', tmpstr, re.IGNORECASE) + if match: + count = match.group() + found = True - count = count.lstrip("0") + if not found: + match = re.search('(?<=\(of\s)\d+(?=\))', tmpstr, re.IGNORECASE) + if match: + count = match.group() + found = True - return count - - def getIssueNumber( self, filename ): + count = count.lstrip("0") - # Returns a tuple of issue number string, and start and end indexs in the filename - # (The indexes will be used to split the string up for further parsing) - - found = False - issue = '' - start = 0 - end = 0 - - # first, look for multiple "--", this means it's formatted differently from most: - if "--" in filename: - # the pattern seems to be that anything to left of the first "--" is the series name followed by issue - filename = re.sub("--.*", self.repl, filename) - - elif "__" in filename: - # the pattern seems to be that anything to left of the first "__" is the series name followed by issue - filename = re.sub("__.*", self.repl, filename) + return count - filename = filename.replace("+", " ") - - # replace parenthetical phrases with spaces - filename = re.sub( "\(.*?\)", self.repl, filename) - filename = re.sub( "\[.*?\]", self.repl, filename) + def getIssueNumber(self, filename): - # replace any name seperators with spaces - filename = self.fixSpaces(filename) + # Returns a tuple of issue number string, and start and end indexs in the filename + # (The indexes will be used to split the string up for further parsing) - # remove any "of NN" phrase with spaces (problem: this could break on some titles) - filename = re.sub( "of [\d]+", self.repl, filename) + found = False + issue = '' + start = 0 + end = 0 - #print u"[{0}]".format(filename) - - # we should now have a cleaned up filename version with all the words in - # the same positions as original filename - - # make a list of each word and its position - word_list = list() - for m in re.finditer("\S+", filename): - word_list.append( (m.group(0), m.start(), m.end()) ) - - # remove the first word, since it can't be the issue number - if len(word_list) > 1: - word_list = word_list[1:] - else: - #only one word?? just bail. - return issue, start, end - - # Now try to search for the likely issue number word in the list - - # first look for a word with "#" followed by digits with optional sufix - # this is almost certainly the issue number - for w in reversed(word_list): - if re.match("#[-]?(([0-9]*\.[0-9]+|[0-9]+)(\w*))", w[0]): - found = True - break + # first, look for multiple "--", this means it's formatted differently from most: + if "--" in filename: + # the pattern seems to be that anything to left of the first "--" is the series name followed by issue + filename = re.sub("--.*", self.repl, filename) - # same as above but w/o a '#', and only look at the last word in the list - if not found: - w = word_list[-1] - if re.match("[-]?(([0-9]*\.[0-9]+|[0-9]+)(\w*))", w[0]): - found = True - - # now try to look for a # followed by any characters - if not found: - for w in reversed(word_list): - if re.match("#\S+", w[0]): - found = True - break - - if found: - issue = w[0] - start = w[1] - end = w[2] - if issue[0] == '#': - issue = issue[1:] - - return issue, start, end - - def getSeriesName(self, filename, issue_start ): - - # use the issue number string index to split the filename string - - if issue_start != 0: - filename = filename[:issue_start] + elif "__" in filename: + # the pattern seems to be that anything to left of the first "__" is the series name followed by issue + filename = re.sub("__.*", self.repl, filename) - # in case there is no issue number, remove some obvious stuff - if "--" in filename: - # the pattern seems to be that anything to left of the first "--" is the series name followed by issue - filename = re.sub("--.*", self.repl, filename) - - elif "__" in filename: - # the pattern seems to be that anything to left of the first "__" is the series name followed by issue - filename = re.sub("__.*", self.repl, filename) - - filename = filename.replace("+", " ") - tmpstr = self.fixSpaces(filename, remove_dashes=False) - - series = tmpstr - volume = "" + filename = filename.replace("+", " ") - #save the last word - try: - last_word = series.split()[-1] - except: - last_word = "" - - # remove any parenthetical phrases - series = re.sub( "\(.*?\)", "", series) - - # search for volume number - match = re.search('(.+)([vV]|[Vv][oO][Ll]\.?\s?)(\d+)\s*$', series) - if match: - series = match.group(1) - volume = match.group(3) - - # if a volume wasn't found, see if the last word is a year in parentheses - # since that's a common way to designate the volume - if volume == "": - #match either (YEAR), (YEAR-), or (YEAR-YEAR2) - match = re.search("(\()(\d{4})(-(\d{4}|)|)(\))", last_word) - if match: - volume = match.group(2) + # replace parenthetical phrases with spaces + filename = re.sub("\(.*?\)", self.repl, filename) + filename = re.sub("\[.*?\]", self.repl, filename) - series = series.strip() + # replace any name seperators with spaces + filename = self.fixSpaces(filename) - # if we don't have an issue number (issue_start==0), look - # for hints i.e. "TPB", "one-shot", "OS", "OGN", etc that might - # be removed to help search online - if issue_start == 0: - one_shot_words = [ "tpb", "os", "one-shot", "ogn", "gn" ] - try: - last_word = series.split()[-1] - if last_word.lower() in one_shot_words: - series = series.rsplit(' ', 1)[0] - except: - pass - - return series, volume.strip() + # remove any "of NN" phrase with spaces (problem: this could break on some titles) + filename = re.sub("of [\d]+", self.repl, filename) - def getYear( self,filename, issue_end): - - filename = filename[issue_end:] + #print u"[{0}]".format(filename) - year = "" - # look for four digit number with "(" ")" or "--" around it - match = re.search('(\(\d\d\d\d\))|(--\d\d\d\d--)', filename) - if match: - year = match.group() - # remove non-numerics - year = re.sub("[^0-9]", "", year) - return year + # we should now have a cleaned up filename version with all the words in + # the same positions as original filename - def getRemainder( self, filename, year, count, issue_end ): - - #make a guess at where the the non-interesting stuff begins - remainder = "" - - if "--" in filename: - remainder = filename.split("--",1)[1] - elif "__" in filename: - remainder = filename.split("__",1)[1] - elif issue_end != 0: - remainder = filename[issue_end:] + # make a list of each word and its position + word_list = list() + for m in re.finditer("\S+", filename): + word_list.append((m.group(0), m.start(), m.end())) - remainder = self.fixSpaces(remainder, remove_dashes=False) - if year != "": - remainder = remainder.replace(year,"",1) - if count != "": - remainder = remainder.replace("of "+count,"",1) - - remainder = remainder.replace("()","") - - return remainder.strip() - - def parseFilename( self, filename ): + # remove the first word, since it can't be the issue number + if len(word_list) > 1: + word_list = word_list[1:] + else: + #only one word?? just bail. + return issue, start, end - # remove the path - filename = os.path.basename(filename) + # Now try to search for the likely issue number word in the list - # remove the extension - filename = os.path.splitext(filename)[0] + # first look for a word with "#" followed by digits with optional sufix + # this is almost certainly the issue number + for w in reversed(word_list): + if re.match("#[-]?(([0-9]*\.[0-9]+|[0-9]+)(\w*))", w[0]): + found = True + break - #url decode, just in case - filename = unquote(filename) + # same as above but w/o a '#', and only look at the last word in the list + if not found: + w = word_list[-1] + if re.match("[-]?(([0-9]*\.[0-9]+|[0-9]+)(\w*))", w[0]): + found = True - # sometimes archives get messed up names from too many decodings - # often url encodings will break and leave "_28" and "_29" in place - # of "(" and ")" see if there are a number of these, and replace them - if filename.count("_28") > 1 and filename.count("_29") > 1: - filename = filename.replace("_28", "(") - filename = filename.replace("_29", ")") - - self.issue, issue_start, issue_end = self.getIssueNumber(filename) - self.series, self.volume = self.getSeriesName(filename, issue_start) - self.year = self.getYear(filename, issue_end) - self.issue_count = self.getIssueCount(filename, issue_end) - self.remainder = self.getRemainder( filename, self.year, self.issue_count, issue_end ) - - if self.issue != "": - # strip off leading zeros - self.issue = self.issue.lstrip("0") - if self.issue == "": - self.issue = "0" - if self.issue[0] == ".": - self.issue = "0" + self.issue + # now try to look for a # followed by any characters + if not found: + for w in reversed(word_list): + if re.match("#\S+", w[0]): + found = True + break + + if found: + issue = w[0] + start = w[1] + end = w[2] + if issue[0] == '#': + issue = issue[1:] + + return issue, start, end + + def getSeriesName(self, filename, issue_start): + + # use the issue number string index to split the filename string + + if issue_start != 0: + filename = filename[:issue_start] + + # in case there is no issue number, remove some obvious stuff + if "--" in filename: + # the pattern seems to be that anything to left of the first "--" is the series name followed by issue + filename = re.sub("--.*", self.repl, filename) + + elif "__" in filename: + # the pattern seems to be that anything to left of the first "__" is the series name followed by issue + filename = re.sub("__.*", self.repl, filename) + + filename = filename.replace("+", " ") + tmpstr = self.fixSpaces(filename, remove_dashes=False) + + series = tmpstr + volume = "" + + #save the last word + try: + last_word = series.split()[-1] + except: + last_word = "" + + # remove any parenthetical phrases + series = re.sub("\(.*?\)", "", series) + + # search for volume number + match = re.search('(.+)([vV]|[Vv][oO][Ll]\.?\s?)(\d+)\s*$', series) + if match: + series = match.group(1) + volume = match.group(3) + + # if a volume wasn't found, see if the last word is a year in parentheses + # since that's a common way to designate the volume + if volume == "": + #match either (YEAR), (YEAR-), or (YEAR-YEAR2) + match = re.search("(\()(\d{4})(-(\d{4}|)|)(\))", last_word) + if match: + volume = match.group(2) + + series = series.strip() + + # if we don't have an issue number (issue_start==0), look + # for hints i.e. "TPB", "one-shot", "OS", "OGN", etc that might + # be removed to help search online + if issue_start == 0: + one_shot_words = ["tpb", "os", "one-shot", "ogn", "gn"] + try: + last_word = series.split()[-1] + if last_word.lower() in one_shot_words: + series = series.rsplit(' ', 1)[0] + except: + pass + + return series, volume.strip() + + def getYear(self, filename, issue_end): + + filename = filename[issue_end:] + + year = "" + # look for four digit number with "(" ")" or "--" around it + match = re.search('(\(\d\d\d\d\))|(--\d\d\d\d--)', filename) + if match: + year = match.group() + # remove non-numerics + year = re.sub("[^0-9]", "", year) + return year + + def getRemainder(self, filename, year, count, issue_end): + + #make a guess at where the the non-interesting stuff begins + remainder = "" + + if "--" in filename: + remainder = filename.split("--", 1)[1] + elif "__" in filename: + remainder = filename.split("__", 1)[1] + elif issue_end != 0: + remainder = filename[issue_end:] + + remainder = self.fixSpaces(remainder, remove_dashes=False) + if year != "": + remainder = remainder.replace(year, "", 1) + if count != "": + remainder = remainder.replace("of " + count, "", 1) + + remainder = remainder.replace("()", "") + + return remainder.strip() + + def parseFilename(self, filename): + + # remove the path + filename = os.path.basename(filename) + + # remove the extension + filename = os.path.splitext(filename)[0] + + #url decode, just in case + filename = unquote(filename) + + # sometimes archives get messed up names from too many decodings + # often url encodings will break and leave "_28" and "_29" in place + # of "(" and ")" see if there are a number of these, and replace them + if filename.count("_28") > 1 and filename.count("_29") > 1: + filename = filename.replace("_28", "(") + filename = filename.replace("_29", ")") + + self.issue, issue_start, issue_end = self.getIssueNumber(filename) + self.series, self.volume = self.getSeriesName(filename, issue_start) + self.year = self.getYear(filename, issue_end) + self.issue_count = self.getIssueCount(filename, issue_end) + self.remainder = self.getRemainder(filename, self.year, + self.issue_count, issue_end) + + if self.issue != "": + # strip off leading zeros + self.issue = self.issue.lstrip("0") + if self.issue == "": + self.issue = "0" + if self.issue[0] == ".": + self.issue = "0" + self.issue diff --git a/genericmetadata.py b/genericmetadata.py old mode 100644 new mode 100755 index 2fe4fe5..05fa627 --- a/genericmetadata.py +++ b/genericmetadata.py @@ -5,9 +5,6 @@ tagging schemes and databases, such as ComicVine or GCD. This makes conversion possible, however lossy it might be -""" - -""" Copyright 2012-2014 Anthony Beville Licensed under the Apache License, Version 2.0 (the "License"); @@ -25,19 +22,21 @@ limitations under the License. import comicapi.utils + # These page info classes are exactly the same as the CIX scheme, since it's unique class PageType: - FrontCover = "FrontCover" - InnerCover = "InnerCover" - Roundup = "Roundup" - Story = "Story" - Advertisement = "Advertisement" - Editorial = "Editorial" - Letters = "Letters" - Preview = "Preview" - BackCover = "BackCover" - Other = "Other" - Deleted = "Deleted" + FrontCover = "FrontCover" + InnerCover = "InnerCover" + Roundup = "Roundup" + Story = "Story" + Advertisement = "Advertisement" + Editorial = "Editorial" + Letters = "Letters" + Preview = "Preview" + BackCover = "BackCover" + Other = "Other" + Deleted = "Deleted" + """ class PageInfo: @@ -48,269 +47,267 @@ class PageInfo: Key = "" ImageWidth = 0 ImageHeight = 0 -""" +""" + class GenericMetadata: + def __init__(self): - def __init__(self): - - self.isEmpty = True - self.tagOrigin = None - - self.series = None - self.issue = None - self.title = None - self.publisher = None - self.month = None - self.year = None - self.day = None - self.issueCount = None - self.volume = None - self.genre = None - self.language = None # 2 letter iso code - self.comments = None # use same way as Summary in CIX + self.isEmpty = True + self.tagOrigin = None - self.volumeCount = None - self.criticalRating = None - self.country = None - - self.alternateSeries = None - self.alternateNumber = None - self.alternateCount = None - self.imprint = None - self.notes = None - self.webLink = None - self.format = None - self.manga = None - self.blackAndWhite = None - self.pageCount = None - self.maturityRating = None - - self.storyArc = None - self.seriesGroup = None - self.scanInfo = None - - self.characters = None - self.teams = None - self.locations = None + self.series = None + self.issue = None + self.title = None + self.publisher = None + self.month = None + self.year = None + self.day = None + self.issueCount = None + self.volume = None + self.genre = None + self.language = None # 2 letter iso code + self.comments = None # use same way as Summary in CIX - self.credits = list() - self.tags = list() - self.pages = list() + self.volumeCount = None + self.criticalRating = None + self.country = None - # Some CoMet-only items - self.price = None - self.isVersionOf = None - self.rights = None - self.identifier = None - self.lastMark = None - self.coverImage = None + self.alternateSeries = None + self.alternateNumber = None + self.alternateCount = None + self.imprint = None + self.notes = None + self.webLink = None + self.format = None + self.manga = None + self.blackAndWhite = None + self.pageCount = None + self.maturityRating = None - def overlay( self, new_md ): - # Overlay a metadata object on this one - # that is, when the new object has non-None - # values, over-write them to this one - - def assign( cur, new ): - if new is not None: - if type(new) == str and len(new) == 0: - setattr(self, cur, None) - else: - setattr(self, cur, new) - - if not new_md.isEmpty: - self.isEmpty = False - - assign( 'series', new_md.series ) - assign( "issue", new_md.issue ) - assign( "issueCount", new_md.issueCount ) - assign( "title", new_md.title ) - assign( "publisher", new_md.publisher ) - assign( "day", new_md.day ) - assign( "month", new_md.month ) - assign( "year", new_md.year ) - assign( "volume", new_md.volume ) - assign( "volumeCount", new_md.volumeCount ) - assign( "genre", new_md.genre ) - assign( "language", new_md.language ) - assign( "country", new_md.country ) - assign( "criticalRating", new_md.criticalRating ) - assign( "alternateSeries", new_md.alternateSeries ) - assign( "alternateNumber", new_md.alternateNumber ) - assign( "alternateCount", new_md.alternateCount ) - assign( "imprint", new_md.imprint ) - assign( "webLink", new_md.webLink ) - assign( "format", new_md.format ) - assign( "manga", new_md.manga ) - assign( "blackAndWhite", new_md.blackAndWhite ) - assign( "maturityRating", new_md.maturityRating ) - assign( "storyArc", new_md.storyArc ) - assign( "seriesGroup", new_md.seriesGroup ) - assign( "scanInfo", new_md.scanInfo ) - assign( "characters", new_md.characters ) - assign( "teams", new_md.teams ) - assign( "locations", new_md.locations ) - assign( "comments", new_md.comments ) - assign( "notes", new_md.notes ) + self.storyArc = None + self.seriesGroup = None + self.scanInfo = None - assign( "price", new_md.price ) - assign( "isVersionOf", new_md.isVersionOf ) - assign( "rights", new_md.rights ) - assign( "identifier", new_md.identifier ) - assign( "lastMark", new_md.lastMark ) - - self.overlayCredits( new_md.credits ) - # TODO - - # not sure if the tags and pages should broken down, or treated - # as whole lists.... + self.characters = None + self.teams = None + self.locations = None - # For now, go the easy route, where any overlay - # value wipes out the whole list - if len(new_md.tags) > 0: - assign( "tags", new_md.tags ) - - if len(new_md.pages) > 0: - assign( "pages", new_md.pages ) + self.credits = list() + self.tags = list() + self.pages = list() - - def overlayCredits( self, new_credits ): - for c in new_credits: - if c.has_key('primary') and c['primary']: - primary = True - else: - primary = False + # Some CoMet-only items + self.price = None + self.isVersionOf = None + self.rights = None + self.identifier = None + self.lastMark = None + self.coverImage = None - # Remove credit role if person is blank - if c['person'] == "": - for r in reversed(self.credits): - if r['role'].lower() == c['role'].lower(): - self.credits.remove(r) - # otherwise, add it! - else: - self.addCredit( c['person'], c['role'], primary ) - - def setDefaultPageList( self, count ): - # generate a default page list, with the first page marked as the cover - for i in range(count): - page_dict = dict() - page_dict['Image'] = str(i) - if i == 0: - page_dict['Type'] = PageType.FrontCover - self.pages.append( page_dict ) + def overlay(self, new_md): + # Overlay a metadata object on this one + # that is, when the new object has non-None + # values, over-write them to this one - def getArchivePageIndex( self, pagenum ): - # convert the displayed page number to the page index of the file in the archive - if pagenum < len( self.pages ): - return int( self.pages[pagenum]['Image'] ) - else: - return 0 - - def getCoverPageIndexList( self ): - # return a list of archive page indices of cover pages - coverlist = [] - for p in self.pages: - if 'Type' in p and p['Type'] == PageType.FrontCover: - coverlist.append( int(p['Image'])) - - if len(coverlist) == 0: - coverlist.append( 0 ) - - return coverlist - - def addCredit( self, person, role, primary = False ): - - credit = dict() - credit['person'] = person - credit['role'] = role - if primary: - credit['primary'] = primary + def assign(cur, new): + if new is not None: + if type(new) == str and len(new) == 0: + setattr(self, cur, None) + else: + setattr(self, cur, new) - # look to see if it's not already there... - found = False - for c in self.credits: - if ( c['person'].lower() == person.lower() and - c['role'].lower() == role.lower() ): - # no need to add it. just adjust the "primary" flag as needed - c['primary'] = primary - found = True - break - - if not found: - self.credits.append(credit) + if not new_md.isEmpty: + self.isEmpty = False - - def __str__( self ): - vals = [] - if self.isEmpty: - return "No metadata" + assign('series', new_md.series) + assign("issue", new_md.issue) + assign("issueCount", new_md.issueCount) + assign("title", new_md.title) + assign("publisher", new_md.publisher) + assign("day", new_md.day) + assign("month", new_md.month) + assign("year", new_md.year) + assign("volume", new_md.volume) + assign("volumeCount", new_md.volumeCount) + assign("genre", new_md.genre) + assign("language", new_md.language) + assign("country", new_md.country) + assign("criticalRating", new_md.criticalRating) + assign("alternateSeries", new_md.alternateSeries) + assign("alternateNumber", new_md.alternateNumber) + assign("alternateCount", new_md.alternateCount) + assign("imprint", new_md.imprint) + assign("webLink", new_md.webLink) + assign("format", new_md.format) + assign("manga", new_md.manga) + assign("blackAndWhite", new_md.blackAndWhite) + assign("maturityRating", new_md.maturityRating) + assign("storyArc", new_md.storyArc) + assign("seriesGroup", new_md.seriesGroup) + assign("scanInfo", new_md.scanInfo) + assign("characters", new_md.characters) + assign("teams", new_md.teams) + assign("locations", new_md.locations) + assign("comments", new_md.comments) + assign("notes", new_md.notes) - def add_string( tag, val ): - if val is not None and u"{0}".format(val) != "": - vals.append( (tag, val) ) + assign("price", new_md.price) + assign("isVersionOf", new_md.isVersionOf) + assign("rights", new_md.rights) + assign("identifier", new_md.identifier) + assign("lastMark", new_md.lastMark) - def add_attr_string( tag ): - val = getattr(self,tag) - add_string( tag, getattr(self,tag) ) + self.overlayCredits(new_md.credits) + # TODO - add_attr_string( "series" ) - add_attr_string( "issue" ) - add_attr_string( "issueCount" ) - add_attr_string( "title" ) - add_attr_string( "publisher" ) - add_attr_string( "year" ) - add_attr_string( "month" ) - add_attr_string( "day" ) - add_attr_string( "volume" ) - add_attr_string( "volumeCount" ) - add_attr_string( "genre" ) - add_attr_string( "language" ) - add_attr_string( "country" ) - add_attr_string( "criticalRating" ) - add_attr_string( "alternateSeries" ) - add_attr_string( "alternateNumber" ) - add_attr_string( "alternateCount" ) - add_attr_string( "imprint" ) - add_attr_string( "webLink" ) - add_attr_string( "format" ) - add_attr_string( "manga" ) + # not sure if the tags and pages should broken down, or treated + # as whole lists.... - add_attr_string( "price" ) - add_attr_string( "isVersionOf" ) - add_attr_string( "rights" ) - add_attr_string( "identifier" ) - add_attr_string( "lastMark" ) - - if self.blackAndWhite: - add_attr_string( "blackAndWhite" ) - add_attr_string( "maturityRating" ) - add_attr_string( "storyArc" ) - add_attr_string( "seriesGroup" ) - add_attr_string( "scanInfo" ) - add_attr_string( "characters" ) - add_attr_string( "teams" ) - add_attr_string( "locations" ) - add_attr_string( "comments" ) - add_attr_string( "notes" ) - - add_string( "tags", comicapi.utils.listToString( self.tags ) ) - - for c in self.credits: - primary = "" - if c.has_key('primary') and c['primary']: - primary = " [P]" - add_string( "credit", c['role']+": "+c['person'] + primary) - - # find the longest field name - flen = 0 - for i in vals: - flen = max( flen, len(i[0]) ) - flen += 1 - - #format the data nicely - outstr = "" - fmt_str = u"{0: <" + str(flen) + "} {1}\n" - for i in vals: - outstr += fmt_str.format( i[0]+":", i[1] ) - - return outstr + # For now, go the easy route, where any overlay + # value wipes out the whole list + if len(new_md.tags) > 0: + assign("tags", new_md.tags) + + if len(new_md.pages) > 0: + assign("pages", new_md.pages) + + def overlayCredits(self, new_credits): + for c in new_credits: + if c.has_key('primary') and c['primary']: + primary = True + else: + primary = False + + # Remove credit role if person is blank + if c['person'] == "": + for r in reversed(self.credits): + if r['role'].lower() == c['role'].lower(): + self.credits.remove(r) + # otherwise, add it! + else: + self.addCredit(c['person'], c['role'], primary) + + def setDefaultPageList(self, count): + # generate a default page list, with the first page marked as the cover + for i in range(count): + page_dict = dict() + page_dict['Image'] = str(i) + if i == 0: + page_dict['Type'] = PageType.FrontCover + self.pages.append(page_dict) + + def getArchivePageIndex(self, pagenum): + # convert the displayed page number to the page index of the file in the archive + if pagenum < len(self.pages): + return int(self.pages[pagenum]['Image']) + else: + return 0 + + def getCoverPageIndexList(self): + # return a list of archive page indices of cover pages + coverlist = [] + for p in self.pages: + if 'Type' in p and p['Type'] == PageType.FrontCover: + coverlist.append(int(p['Image'])) + + if len(coverlist) == 0: + coverlist.append(0) + + return coverlist + + def addCredit(self, person, role, primary=False): + + credit = dict() + credit['person'] = person + credit['role'] = role + if primary: + credit['primary'] = primary + + # look to see if it's not already there... + found = False + for c in self.credits: + if (c['person'].lower() == person.lower() + and c['role'].lower() == role.lower()): + # no need to add it. just adjust the "primary" flag as needed + c['primary'] = primary + found = True + break + + if not found: + self.credits.append(credit) + + def __str__(self): + vals = [] + if self.isEmpty: + return "No metadata" + + def add_string(tag, val): + if val is not None and u"{0}".format(val) != "": + vals.append((tag, val)) + + def add_attr_string(tag): + val = getattr(self, tag) + add_string(tag, getattr(self, tag)) + + add_attr_string("series") + add_attr_string("issue") + add_attr_string("issueCount") + add_attr_string("title") + add_attr_string("publisher") + add_attr_string("year") + add_attr_string("month") + add_attr_string("day") + add_attr_string("volume") + add_attr_string("volumeCount") + add_attr_string("genre") + add_attr_string("language") + add_attr_string("country") + add_attr_string("criticalRating") + add_attr_string("alternateSeries") + add_attr_string("alternateNumber") + add_attr_string("alternateCount") + add_attr_string("imprint") + add_attr_string("webLink") + add_attr_string("format") + add_attr_string("manga") + + add_attr_string("price") + add_attr_string("isVersionOf") + add_attr_string("rights") + add_attr_string("identifier") + add_attr_string("lastMark") + + if self.blackAndWhite: + add_attr_string("blackAndWhite") + add_attr_string("maturityRating") + add_attr_string("storyArc") + add_attr_string("seriesGroup") + add_attr_string("scanInfo") + add_attr_string("characters") + add_attr_string("teams") + add_attr_string("locations") + add_attr_string("comments") + add_attr_string("notes") + + add_string("tags", comicapi.utils.listToString(self.tags)) + + for c in self.credits: + primary = "" + if c.has_key('primary') and c['primary']: + primary = " [P]" + add_string("credit", c['role'] + ": " + c['person'] + primary) + + # find the longest field name + flen = 0 + for i in vals: + flen = max(flen, len(i[0])) + flen += 1 + + #format the data nicely + outstr = "" + fmt_str = u"{0: <" + str(flen) + "} {1}\n" + for i in vals: + outstr += fmt_str.format(i[0] + ":", i[1]) + + return outstr diff --git a/issuestring.py b/issuestring.py old mode 100644 new mode 100755 index 62c0b0a..b50454b --- a/issuestring.py +++ b/issuestring.py @@ -11,9 +11,6 @@ e.g.: "5AU" "100-2" -""" - -""" Copyright 2012-2014 Anthony Beville Licensed under the Apache License, Version 2.0 (the "License"); @@ -33,106 +30,105 @@ import comicapi.utils import math import re + class IssueString: - def __init__(self, text): - - # break up the issue number string into 2 parts: the numeric and suffix string. - # ( assumes that the numeric portion is always first ) - - self.num = None - self.suffix = "" + def __init__(self, text): - if text is None: - return + # break up the issue number string into 2 parts: the numeric and suffix string. + # ( assumes that the numeric portion is always first ) - if type(text) == int: - text = str(text) + self.num = None + self.suffix = "" - if len(text) == 0: - return - - #skip the minus sign if it's first - if text[0] == '-': - start = 1 - else: - start = 0 + if text is None: + return - # if it's still not numeric at start skip it - if text[start].isdigit() or text[start] == ".": - # walk through the string, look for split point (the first non-numeric) - decimal_count = 0 - for idx in range( start, len(text) ): - if text[idx] not in "0123456789.": - break - # special case: also split on second "." - if text[idx] == ".": - decimal_count += 1 - if decimal_count > 1: - break - else: - idx = len(text) - - # move trailing numeric decimal to suffix - # (only if there is other junk after ) - if text[idx-1] == "." and len(text) != idx: - idx = idx -1 - - # if there is no numeric after the minus, make the minus part of the suffix - if idx == 1 and start == 1: - idx = 0 - - part1 = text[0:idx] - part2 = text[idx:len(text)] - - if part1 != "": - self.num = float( part1 ) - self.suffix = part2 - else: - self.suffix = text - - #print "num: {0} suf: {1}".format(self.num, self.suffix) + if type(text) == int: + text = str(text) - def asString( self, pad = 0 ): - #return the float, left side zero-padded, with suffix attached - if self.num is None: - return self.suffix - - negative = self.num < 0 + if len(text) == 0: + return - num_f = abs(self.num) - - num_int = int( num_f ) - num_s = str( num_int ) - if float( num_int ) != num_f: - num_s = str( num_f ) - - num_s += self.suffix - - # create padding - padding = "" - l = len( str(num_int)) - if l < pad : - padding = "0" * (pad - l) - - num_s = padding + num_s - if negative: - num_s = "-" + num_s + #skip the minus sign if it's first + if text[0] == '-': + start = 1 + else: + start = 0 - return num_s - - def asFloat( self ): - #return the float, with no suffix - if self.suffix == u"½": - if self.num is not None: - return self.num + .5 - else: - return .5 - return self.num - - def asInt( self ): - #return the int version of the float - if self.num is None: - return None - return int( self.num ) - - + # if it's still not numeric at start skip it + if text[start].isdigit() or text[start] == ".": + # walk through the string, look for split point (the first non-numeric) + decimal_count = 0 + for idx in range(start, len(text)): + if text[idx] not in "0123456789.": + break + # special case: also split on second "." + if text[idx] == ".": + decimal_count += 1 + if decimal_count > 1: + break + else: + idx = len(text) + + # move trailing numeric decimal to suffix + # (only if there is other junk after ) + if text[idx - 1] == "." and len(text) != idx: + idx = idx - 1 + + # if there is no numeric after the minus, make the minus part of the suffix + if idx == 1 and start == 1: + idx = 0 + + part1 = text[0:idx] + part2 = text[idx:len(text)] + + if part1 != "": + self.num = float(part1) + self.suffix = part2 + else: + self.suffix = text + + #print "num: {0} suf: {1}".format(self.num, self.suffix) + + def asString(self, pad=0): + #return the float, left side zero-padded, with suffix attached + if self.num is None: + return self.suffix + + negative = self.num < 0 + + num_f = abs(self.num) + + num_int = int(num_f) + num_s = str(num_int) + if float(num_int) != num_f: + num_s = str(num_f) + + num_s += self.suffix + + # create padding + padding = "" + l = len(str(num_int)) + if l < pad: + padding = "0" * (pad - l) + + num_s = padding + num_s + if negative: + num_s = "-" + num_s + + return num_s + + def asFloat(self): + #return the float, with no suffix + if self.suffix == u"½": + if self.num is not None: + return self.num + .5 + else: + return .5 + return self.num + + def asInt(self): + #return the int version of the float + if self.num is None: + return None + return int(self.num) diff --git a/utils.py b/utils.py old mode 100644 new mode 100755 index ded8efa..a02932d --- a/utils.py +++ b/utils.py @@ -2,10 +2,7 @@ """ Some generic utilities -""" - -""" Copyright 2012-2014 Anthony Beville Licensed under the Apache License, Version 2.0 (the "License"); @@ -29,569 +26,559 @@ import codecs class UtilsVars: - already_fixed_encoding = False + already_fixed_encoding = False def get_actual_preferred_encoding(): - preferred_encoding = locale.getpreferredencoding() - if platform.system() == "Darwin": - preferred_encoding = "utf-8" - return preferred_encoding - -def fix_output_encoding( ): - if not UtilsVars.already_fixed_encoding: - # this reads the environment and inits the right locale - locale.setlocale(locale.LC_ALL, "") + preferred_encoding = locale.getpreferredencoding() + if platform.system() == "Darwin": + preferred_encoding = "utf-8" + return preferred_encoding - # try to make stdout/stderr encodings happy for unicode printing - preferred_encoding = get_actual_preferred_encoding() - sys.stdout = codecs.getwriter(preferred_encoding)(sys.stdout) - sys.stderr = codecs.getwriter(preferred_encoding)(sys.stderr) - UtilsVars.already_fixed_encoding = True +def fix_output_encoding( ): + if not UtilsVars.already_fixed_encoding: + # this reads the environment and inits the right locale + locale.setlocale(locale.LC_ALL, "") + + # try to make stdout/stderr encodings happy for unicode printing + preferred_encoding = get_actual_preferred_encoding() + sys.stdout = codecs.getwriter(preferred_encoding)(sys.stdout) + sys.stderr = codecs.getwriter(preferred_encoding)(sys.stderr) + UtilsVars.already_fixed_encoding = True def get_recursive_filelist( pathlist ): - """ + """ Get a recursive list of of all files under all path items in the list """ - filename_encoding = sys.getfilesystemencoding() - filelist = [] - for p in pathlist: - # if path is a folder, walk it recursivly, and all files underneath - if type(p) == str: - #make sure string is unicode - p = p.decode(filename_encoding) #, 'replace') - elif type(p) != str: - #it's probably a QString - p = str(p) - - if os.path.isdir( p ): - for root,dirs,files in os.walk( p ): - for f in files: - if type(f) == str: - #make sure string is unicode - f = f.decode(filename_encoding, 'replace') - elif type(f) != str: - #it's probably a QString - f = str(f) - filelist.append(os.path.join(root,f)) - else: - filelist.append(p) - - return filelist - + filename_encoding = sys.getfilesystemencoding() + filelist = [] + for p in pathlist: + # if path is a folder, walk it recursivly, and all files underneath + if type(p) == str: + #make sure string is unicode + p = p.decode(filename_encoding) #, 'replace') + elif type(p) != str: + #it's probably a QString + p = str(p) + + if os.path.isdir( p ): + for root,dirs,files in os.walk( p ): + for f in files: + if type(f) == str: + #make sure string is unicode + f = f.decode(filename_encoding, 'replace') + elif type(f) != str: + #it's probably a QString + f = str(f) + filelist.append(os.path.join(root,f)) + else: + filelist.append(p) + + return filelist + def listToString( l ): - string = "" - if l is not None: - for item in l: - if len(string) > 0: - string += ", " - string += item - return string - + string = "" + if l is not None: + for item in l: + if len(string) > 0: + string += ", " + string += item + return string + def addtopath( dirname ): - if dirname is not None and dirname != "": - - # verify that path doesn't already contain the given dirname - tmpdirname = re.escape(dirname) - pattern = r"{sep}{dir}$|^{dir}{sep}|{sep}{dir}{sep}|^{dir}$".format( dir=tmpdirname, sep=os.pathsep) - - match = re.search(pattern, os.environ['PATH']) - if not match: - os.environ['PATH'] = dirname + os.pathsep + os.environ['PATH'] + if dirname is not None and dirname != "": + + # verify that path doesn't already contain the given dirname + tmpdirname = re.escape(dirname) + pattern = r"{sep}{dir}$|^{dir}{sep}|{sep}{dir}{sep}|^{dir}$".format( dir=tmpdirname, sep=os.pathsep) + + match = re.search(pattern, os.environ['PATH']) + if not match: + os.environ['PATH'] = dirname + os.pathsep + os.environ['PATH'] # returns executable path, if it exists def which(program): - - def is_exe(fpath): - return os.path.isfile(fpath) and os.access(fpath, os.X_OK) - fpath, fname = os.path.split(program) - if fpath: - if is_exe(program): - return program - else: - for path in os.environ["PATH"].split(os.pathsep): - exe_file = os.path.join(path, program) - if is_exe(exe_file): - return exe_file + def is_exe(fpath): + return os.path.isfile(fpath) and os.access(fpath, os.X_OK) - return None + fpath, fname = os.path.split(program) + if fpath: + if is_exe(program): + return program + else: + for path in os.environ["PATH"].split(os.pathsep): + exe_file = os.path.join(path, program) + if is_exe(exe_file): + return exe_file + + return None def removearticles( text ): - text = text.lower() - articles = ['and', 'the', 'a', '&', 'issue' ] - newText = '' - for word in text.split(' '): - if word not in articles: - newText += word+' ' - - newText = newText[:-1] - - # now get rid of some other junk - newText = newText.replace(":", "") - newText = newText.replace(",", "") - newText = newText.replace("-", " ") + text = text.lower() + articles = ['and', 'the', 'a', '&', 'issue' ] + newText = '' + for word in text.split(' '): + if word not in articles: + newText += word+' ' - # since the CV api changed, searches for series names with periods - # now explicity require the period to be in the search key, - # so the line below is removed (for now) - #newText = newText.replace(".", "") - - return newText + newText = newText[:-1] + + # now get rid of some other junk + newText = newText.replace(":", "") + newText = newText.replace(",", "") + newText = newText.replace("-", " ") + + # since the CV api changed, searches for series names with periods + # now explicity require the period to be in the search key, + # so the line below is removed (for now) + #newText = newText.replace(".", "") + + return newText def unique_file(file_name): - counter = 1 - file_name_parts = os.path.splitext(file_name) # returns ('/path/file', '.ext') - while 1: - if not os.path.lexists( file_name): - return file_name - file_name = file_name_parts[0] + ' (' + str(counter) + ')' + file_name_parts[1] - counter += 1 + counter = 1 + file_name_parts = os.path.splitext(file_name) # returns ('/path/file', '.ext') + while 1: + if not os.path.lexists( file_name): + return file_name + file_name = file_name_parts[0] + ' (' + str(counter) + ')' + file_name_parts[1] + counter += 1 # -o- coding: utf-8 -o- -# ISO639 python dict +# ISO639 python dict # oficial list in http://www.loc.gov/standards/iso639-2/php/code_list.php lang_dict = { - 'ab': 'Abkhaz', - 'aa': 'Afar', - 'af': 'Afrikaans', - 'ak': 'Akan', - 'sq': 'Albanian', - 'am': 'Amharic', - 'ar': 'Arabic', - 'an': 'Aragonese', - 'hy': 'Armenian', - 'as': 'Assamese', - 'av': 'Avaric', - 'ae': 'Avestan', - 'ay': 'Aymara', - 'az': 'Azerbaijani', - 'bm': 'Bambara', - 'ba': 'Bashkir', - 'eu': 'Basque', - 'be': 'Belarusian', - 'bn': 'Bengali', - 'bh': 'Bihari', - 'bi': 'Bislama', - 'bs': 'Bosnian', - 'br': 'Breton', - 'bg': 'Bulgarian', - 'my': 'Burmese', - 'ca': 'Catalan; Valencian', - 'ch': 'Chamorro', - 'ce': 'Chechen', - 'ny': 'Chichewa; Chewa; Nyanja', - 'zh': 'Chinese', - 'cv': 'Chuvash', - 'kw': 'Cornish', - 'co': 'Corsican', - 'cr': 'Cree', - 'hr': 'Croatian', - 'cs': 'Czech', - 'da': 'Danish', - 'dv': 'Divehi; Maldivian;', - 'nl': 'Dutch', - 'dz': 'Dzongkha', - 'en': 'English', - 'eo': 'Esperanto', - 'et': 'Estonian', - 'ee': 'Ewe', - 'fo': 'Faroese', - 'fj': 'Fijian', - 'fi': 'Finnish', - 'fr': 'French', - 'ff': 'Fula', - 'gl': 'Galician', - 'ka': 'Georgian', - 'de': 'German', - 'el': 'Greek, Modern', - 'gn': 'Guaraní', - 'gu': 'Gujarati', - 'ht': 'Haitian', - 'ha': 'Hausa', - 'he': 'Hebrew (modern)', - 'hz': 'Herero', - 'hi': 'Hindi', - 'ho': 'Hiri Motu', - 'hu': 'Hungarian', - 'ia': 'Interlingua', - 'id': 'Indonesian', - 'ie': 'Interlingue', - 'ga': 'Irish', - 'ig': 'Igbo', - 'ik': 'Inupiaq', - 'io': 'Ido', - 'is': 'Icelandic', - 'it': 'Italian', - 'iu': 'Inuktitut', - 'ja': 'Japanese', - 'jv': 'Javanese', - 'kl': 'Kalaallisut', - 'kn': 'Kannada', - 'kr': 'Kanuri', - 'ks': 'Kashmiri', - 'kk': 'Kazakh', - 'km': 'Khmer', - 'ki': 'Kikuyu, Gikuyu', - 'rw': 'Kinyarwanda', - 'ky': 'Kirghiz, Kyrgyz', - 'kv': 'Komi', - 'kg': 'Kongo', - 'ko': 'Korean', - 'ku': 'Kurdish', - 'kj': 'Kwanyama, Kuanyama', - 'la': 'Latin', - 'lb': 'Luxembourgish', - 'lg': 'Luganda', - 'li': 'Limburgish', - 'ln': 'Lingala', - 'lo': 'Lao', - 'lt': 'Lithuanian', - 'lu': 'Luba-Katanga', - 'lv': 'Latvian', - 'gv': 'Manx', - 'mk': 'Macedonian', - 'mg': 'Malagasy', - 'ms': 'Malay', - 'ml': 'Malayalam', - 'mt': 'Maltese', - 'mi': 'Māori', - 'mr': 'Marathi (Marāṭhī)', - 'mh': 'Marshallese', - 'mn': 'Mongolian', - 'na': 'Nauru', - 'nv': 'Navajo, Navaho', - 'nb': 'Norwegian Bokmål', - 'nd': 'North Ndebele', - 'ne': 'Nepali', - 'ng': 'Ndonga', - 'nn': 'Norwegian Nynorsk', - 'no': 'Norwegian', - 'ii': 'Nuosu', - 'nr': 'South Ndebele', - 'oc': 'Occitan', - 'oj': 'Ojibwe, Ojibwa', - 'cu': 'Old Church Slavonic', - 'om': 'Oromo', - 'or': 'Oriya', - 'os': 'Ossetian, Ossetic', - 'pa': 'Panjabi, Punjabi', - 'pi': 'Pāli', - 'fa': 'Persian', - 'pl': 'Polish', - 'ps': 'Pashto, Pushto', - 'pt': 'Portuguese', - 'qu': 'Quechua', - 'rm': 'Romansh', - 'rn': 'Kirundi', - 'ro': 'Romanian, Moldavan', - 'ru': 'Russian', - 'sa': 'Sanskrit (Saṁskṛta)', - 'sc': 'Sardinian', - 'sd': 'Sindhi', - 'se': 'Northern Sami', - 'sm': 'Samoan', - 'sg': 'Sango', - 'sr': 'Serbian', - 'gd': 'Scottish Gaelic', - 'sn': 'Shona', - 'si': 'Sinhala, Sinhalese', - 'sk': 'Slovak', - 'sl': 'Slovene', - 'so': 'Somali', - 'st': 'Southern Sotho', - 'es': 'Spanish; Castilian', - 'su': 'Sundanese', - 'sw': 'Swahili', - 'ss': 'Swati', - 'sv': 'Swedish', - 'ta': 'Tamil', - 'te': 'Telugu', - 'tg': 'Tajik', - 'th': 'Thai', - 'ti': 'Tigrinya', - 'bo': 'Tibetan', - 'tk': 'Turkmen', - 'tl': 'Tagalog', - 'tn': 'Tswana', - 'to': 'Tonga', - 'tr': 'Turkish', - 'ts': 'Tsonga', - 'tt': 'Tatar', - 'tw': 'Twi', - 'ty': 'Tahitian', - 'ug': 'Uighur, Uyghur', - 'uk': 'Ukrainian', - 'ur': 'Urdu', - 'uz': 'Uzbek', - 've': 'Venda', - 'vi': 'Vietnamese', - 'vo': 'Volapük', - 'wa': 'Walloon', - 'cy': 'Welsh', - 'wo': 'Wolof', - 'fy': 'Western Frisian', - 'xh': 'Xhosa', - 'yi': 'Yiddish', - 'yo': 'Yoruba', - 'za': 'Zhuang, Chuang', - 'zu': 'Zulu', + 'ab': 'Abkhaz', + 'aa': 'Afar', + 'af': 'Afrikaans', + 'ak': 'Akan', + 'sq': 'Albanian', + 'am': 'Amharic', + 'ar': 'Arabic', + 'an': 'Aragonese', + 'hy': 'Armenian', + 'as': 'Assamese', + 'av': 'Avaric', + 'ae': 'Avestan', + 'ay': 'Aymara', + 'az': 'Azerbaijani', + 'bm': 'Bambara', + 'ba': 'Bashkir', + 'eu': 'Basque', + 'be': 'Belarusian', + 'bn': 'Bengali', + 'bh': 'Bihari', + 'bi': 'Bislama', + 'bs': 'Bosnian', + 'br': 'Breton', + 'bg': 'Bulgarian', + 'my': 'Burmese', + 'ca': 'Catalan; Valencian', + 'ch': 'Chamorro', + 'ce': 'Chechen', + 'ny': 'Chichewa; Chewa; Nyanja', + 'zh': 'Chinese', + 'cv': 'Chuvash', + 'kw': 'Cornish', + 'co': 'Corsican', + 'cr': 'Cree', + 'hr': 'Croatian', + 'cs': 'Czech', + 'da': 'Danish', + 'dv': 'Divehi; Maldivian;', + 'nl': 'Dutch', + 'dz': 'Dzongkha', + 'en': 'English', + 'eo': 'Esperanto', + 'et': 'Estonian', + 'ee': 'Ewe', + 'fo': 'Faroese', + 'fj': 'Fijian', + 'fi': 'Finnish', + 'fr': 'French', + 'ff': 'Fula', + 'gl': 'Galician', + 'ka': 'Georgian', + 'de': 'German', + 'el': 'Greek, Modern', + 'gn': 'Guaraní', + 'gu': 'Gujarati', + 'ht': 'Haitian', + 'ha': 'Hausa', + 'he': 'Hebrew (modern)', + 'hz': 'Herero', + 'hi': 'Hindi', + 'ho': 'Hiri Motu', + 'hu': 'Hungarian', + 'ia': 'Interlingua', + 'id': 'Indonesian', + 'ie': 'Interlingue', + 'ga': 'Irish', + 'ig': 'Igbo', + 'ik': 'Inupiaq', + 'io': 'Ido', + 'is': 'Icelandic', + 'it': 'Italian', + 'iu': 'Inuktitut', + 'ja': 'Japanese', + 'jv': 'Javanese', + 'kl': 'Kalaallisut', + 'kn': 'Kannada', + 'kr': 'Kanuri', + 'ks': 'Kashmiri', + 'kk': 'Kazakh', + 'km': 'Khmer', + 'ki': 'Kikuyu, Gikuyu', + 'rw': 'Kinyarwanda', + 'ky': 'Kirghiz, Kyrgyz', + 'kv': 'Komi', + 'kg': 'Kongo', + 'ko': 'Korean', + 'ku': 'Kurdish', + 'kj': 'Kwanyama, Kuanyama', + 'la': 'Latin', + 'lb': 'Luxembourgish', + 'lg': 'Luganda', + 'li': 'Limburgish', + 'ln': 'Lingala', + 'lo': 'Lao', + 'lt': 'Lithuanian', + 'lu': 'Luba-Katanga', + 'lv': 'Latvian', + 'gv': 'Manx', + 'mk': 'Macedonian', + 'mg': 'Malagasy', + 'ms': 'Malay', + 'ml': 'Malayalam', + 'mt': 'Maltese', + 'mi': 'Māori', + 'mr': 'Marathi (Marāṭhī)', + 'mh': 'Marshallese', + 'mn': 'Mongolian', + 'na': 'Nauru', + 'nv': 'Navajo, Navaho', + 'nb': 'Norwegian Bokmål', + 'nd': 'North Ndebele', + 'ne': 'Nepali', + 'ng': 'Ndonga', + 'nn': 'Norwegian Nynorsk', + 'no': 'Norwegian', + 'ii': 'Nuosu', + 'nr': 'South Ndebele', + 'oc': 'Occitan', + 'oj': 'Ojibwe, Ojibwa', + 'cu': 'Old Church Slavonic', + 'om': 'Oromo', + 'or': 'Oriya', + 'os': 'Ossetian, Ossetic', + 'pa': 'Panjabi, Punjabi', + 'pi': 'Pāli', + 'fa': 'Persian', + 'pl': 'Polish', + 'ps': 'Pashto, Pushto', + 'pt': 'Portuguese', + 'qu': 'Quechua', + 'rm': 'Romansh', + 'rn': 'Kirundi', + 'ro': 'Romanian, Moldavan', + 'ru': 'Russian', + 'sa': 'Sanskrit (Saṁskṛta)', + 'sc': 'Sardinian', + 'sd': 'Sindhi', + 'se': 'Northern Sami', + 'sm': 'Samoan', + 'sg': 'Sango', + 'sr': 'Serbian', + 'gd': 'Scottish Gaelic', + 'sn': 'Shona', + 'si': 'Sinhala, Sinhalese', + 'sk': 'Slovak', + 'sl': 'Slovene', + 'so': 'Somali', + 'st': 'Southern Sotho', + 'es': 'Spanish; Castilian', + 'su': 'Sundanese', + 'sw': 'Swahili', + 'ss': 'Swati', + 'sv': 'Swedish', + 'ta': 'Tamil', + 'te': 'Telugu', + 'tg': 'Tajik', + 'th': 'Thai', + 'ti': 'Tigrinya', + 'bo': 'Tibetan', + 'tk': 'Turkmen', + 'tl': 'Tagalog', + 'tn': 'Tswana', + 'to': 'Tonga', + 'tr': 'Turkish', + 'ts': 'Tsonga', + 'tt': 'Tatar', + 'tw': 'Twi', + 'ty': 'Tahitian', + 'ug': 'Uighur, Uyghur', + 'uk': 'Ukrainian', + 'ur': 'Urdu', + 'uz': 'Uzbek', + 've': 'Venda', + 'vi': 'Vietnamese', + 'vo': 'Volapük', + 'wa': 'Walloon', + 'cy': 'Welsh', + 'wo': 'Wolof', + 'fy': 'Western Frisian', + 'xh': 'Xhosa', + 'yi': 'Yiddish', + 'yo': 'Yoruba', + 'za': 'Zhuang, Chuang', + 'zu': 'Zulu', } countries = [ - ('AF', 'Afghanistan'), - ('AL', 'Albania'), - ('DZ', 'Algeria'), - ('AS', 'American Samoa'), - ('AD', 'Andorra'), - ('AO', 'Angola'), - ('AI', 'Anguilla'), - ('AQ', 'Antarctica'), - ('AG', 'Antigua And Barbuda'), - ('AR', 'Argentina'), - ('AM', 'Armenia'), - ('AW', 'Aruba'), - ('AU', 'Australia'), - ('AT', 'Austria'), - ('AZ', 'Azerbaijan'), - ('BS', 'Bahamas'), - ('BH', 'Bahrain'), - ('BD', 'Bangladesh'), - ('BB', 'Barbados'), - ('BY', 'Belarus'), - ('BE', 'Belgium'), - ('BZ', 'Belize'), - ('BJ', 'Benin'), - ('BM', 'Bermuda'), - ('BT', 'Bhutan'), - ('BO', 'Bolivia'), - ('BA', 'Bosnia And Herzegowina'), - ('BW', 'Botswana'), - ('BV', 'Bouvet Island'), - ('BR', 'Brazil'), - ('BN', 'Brunei Darussalam'), - ('BG', 'Bulgaria'), - ('BF', 'Burkina Faso'), - ('BI', 'Burundi'), - ('KH', 'Cambodia'), - ('CM', 'Cameroon'), - ('CA', 'Canada'), - ('CV', 'Cape Verde'), - ('KY', 'Cayman Islands'), - ('CF', 'Central African Rep'), - ('TD', 'Chad'), - ('CL', 'Chile'), - ('CN', 'China'), - ('CX', 'Christmas Island'), - ('CC', 'Cocos Islands'), - ('CO', 'Colombia'), - ('KM', 'Comoros'), - ('CG', 'Congo'), - ('CK', 'Cook Islands'), - ('CR', 'Costa Rica'), - ('CI', 'Cote D`ivoire'), - ('HR', 'Croatia'), - ('CU', 'Cuba'), - ('CY', 'Cyprus'), - ('CZ', 'Czech Republic'), - ('DK', 'Denmark'), - ('DJ', 'Djibouti'), - ('DM', 'Dominica'), - ('DO', 'Dominican Republic'), - ('TP', 'East Timor'), - ('EC', 'Ecuador'), - ('EG', 'Egypt'), - ('SV', 'El Salvador'), - ('GQ', 'Equatorial Guinea'), - ('ER', 'Eritrea'), - ('EE', 'Estonia'), - ('ET', 'Ethiopia'), - ('FK', 'Falkland Islands (Malvinas)'), - ('FO', 'Faroe Islands'), - ('FJ', 'Fiji'), - ('FI', 'Finland'), - ('FR', 'France'), - ('GF', 'French Guiana'), - ('PF', 'French Polynesia'), - ('TF', 'French S. Territories'), - ('GA', 'Gabon'), - ('GM', 'Gambia'), - ('GE', 'Georgia'), - ('DE', 'Germany'), - ('GH', 'Ghana'), - ('GI', 'Gibraltar'), - ('GR', 'Greece'), - ('GL', 'Greenland'), - ('GD', 'Grenada'), - ('GP', 'Guadeloupe'), - ('GU', 'Guam'), - ('GT', 'Guatemala'), - ('GN', 'Guinea'), - ('GW', 'Guinea-bissau'), - ('GY', 'Guyana'), - ('HT', 'Haiti'), - ('HN', 'Honduras'), - ('HK', 'Hong Kong'), - ('HU', 'Hungary'), - ('IS', 'Iceland'), - ('IN', 'India'), - ('ID', 'Indonesia'), - ('IR', 'Iran'), - ('IQ', 'Iraq'), - ('IE', 'Ireland'), - ('IL', 'Israel'), - ('IT', 'Italy'), - ('JM', 'Jamaica'), - ('JP', 'Japan'), - ('JO', 'Jordan'), - ('KZ', 'Kazakhstan'), - ('KE', 'Kenya'), - ('KI', 'Kiribati'), - ('KP', 'Korea (North)'), - ('KR', 'Korea (South)'), - ('KW', 'Kuwait'), - ('KG', 'Kyrgyzstan'), - ('LA', 'Laos'), - ('LV', 'Latvia'), - ('LB', 'Lebanon'), - ('LS', 'Lesotho'), - ('LR', 'Liberia'), - ('LY', 'Libya'), - ('LI', 'Liechtenstein'), - ('LT', 'Lithuania'), - ('LU', 'Luxembourg'), - ('MO', 'Macau'), - ('MK', 'Macedonia'), - ('MG', 'Madagascar'), - ('MW', 'Malawi'), - ('MY', 'Malaysia'), - ('MV', 'Maldives'), - ('ML', 'Mali'), - ('MT', 'Malta'), - ('MH', 'Marshall Islands'), - ('MQ', 'Martinique'), - ('MR', 'Mauritania'), - ('MU', 'Mauritius'), - ('YT', 'Mayotte'), - ('MX', 'Mexico'), - ('FM', 'Micronesia'), - ('MD', 'Moldova'), - ('MC', 'Monaco'), - ('MN', 'Mongolia'), - ('MS', 'Montserrat'), - ('MA', 'Morocco'), - ('MZ', 'Mozambique'), - ('MM', 'Myanmar'), - ('NA', 'Namibia'), - ('NR', 'Nauru'), - ('NP', 'Nepal'), - ('NL', 'Netherlands'), - ('AN', 'Netherlands Antilles'), - ('NC', 'New Caledonia'), - ('NZ', 'New Zealand'), - ('NI', 'Nicaragua'), - ('NE', 'Niger'), - ('NG', 'Nigeria'), - ('NU', 'Niue'), - ('NF', 'Norfolk Island'), - ('MP', 'Northern Mariana Islands'), - ('NO', 'Norway'), - ('OM', 'Oman'), - ('PK', 'Pakistan'), - ('PW', 'Palau'), - ('PA', 'Panama'), - ('PG', 'Papua New Guinea'), - ('PY', 'Paraguay'), - ('PE', 'Peru'), - ('PH', 'Philippines'), - ('PN', 'Pitcairn'), - ('PL', 'Poland'), - ('PT', 'Portugal'), - ('PR', 'Puerto Rico'), - ('QA', 'Qatar'), - ('RE', 'Reunion'), - ('RO', 'Romania'), - ('RU', 'Russian Federation'), - ('RW', 'Rwanda'), - ('KN', 'Saint Kitts And Nevis'), - ('LC', 'Saint Lucia'), - ('VC', 'St Vincent/Grenadines'), - ('WS', 'Samoa'), - ('SM', 'San Marino'), - ('ST', 'Sao Tome'), - ('SA', 'Saudi Arabia'), - ('SN', 'Senegal'), - ('SC', 'Seychelles'), - ('SL', 'Sierra Leone'), - ('SG', 'Singapore'), - ('SK', 'Slovakia'), - ('SI', 'Slovenia'), - ('SB', 'Solomon Islands'), - ('SO', 'Somalia'), - ('ZA', 'South Africa'), - ('ES', 'Spain'), - ('LK', 'Sri Lanka'), - ('SH', 'St. Helena'), - ('PM', 'St.Pierre'), - ('SD', 'Sudan'), - ('SR', 'Suriname'), - ('SZ', 'Swaziland'), - ('SE', 'Sweden'), - ('CH', 'Switzerland'), - ('SY', 'Syrian Arab Republic'), - ('TW', 'Taiwan'), - ('TJ', 'Tajikistan'), - ('TZ', 'Tanzania'), - ('TH', 'Thailand'), - ('TG', 'Togo'), - ('TK', 'Tokelau'), - ('TO', 'Tonga'), - ('TT', 'Trinidad And Tobago'), - ('TN', 'Tunisia'), - ('TR', 'Turkey'), - ('TM', 'Turkmenistan'), - ('TV', 'Tuvalu'), - ('UG', 'Uganda'), - ('UA', 'Ukraine'), - ('AE', 'United Arab Emirates'), - ('UK', 'United Kingdom'), - ('US', 'United States'), - ('UY', 'Uruguay'), - ('UZ', 'Uzbekistan'), - ('VU', 'Vanuatu'), - ('VA', 'Vatican City State'), - ('VE', 'Venezuela'), - ('VN', 'Viet Nam'), - ('VG', 'Virgin Islands (British)'), - ('VI', 'Virgin Islands (U.S.)'), - ('EH', 'Western Sahara'), - ('YE', 'Yemen'), - ('YU', 'Yugoslavia'), - ('ZR', 'Zaire'), - ('ZM', 'Zambia'), - ('ZW', 'Zimbabwe') + ('AF', 'Afghanistan'), + ('AL', 'Albania'), + ('DZ', 'Algeria'), + ('AS', 'American Samoa'), + ('AD', 'Andorra'), + ('AO', 'Angola'), + ('AI', 'Anguilla'), + ('AQ', 'Antarctica'), + ('AG', 'Antigua And Barbuda'), + ('AR', 'Argentina'), + ('AM', 'Armenia'), + ('AW', 'Aruba'), + ('AU', 'Australia'), + ('AT', 'Austria'), + ('AZ', 'Azerbaijan'), + ('BS', 'Bahamas'), + ('BH', 'Bahrain'), + ('BD', 'Bangladesh'), + ('BB', 'Barbados'), + ('BY', 'Belarus'), + ('BE', 'Belgium'), + ('BZ', 'Belize'), + ('BJ', 'Benin'), + ('BM', 'Bermuda'), + ('BT', 'Bhutan'), + ('BO', 'Bolivia'), + ('BA', 'Bosnia And Herzegowina'), + ('BW', 'Botswana'), + ('BV', 'Bouvet Island'), + ('BR', 'Brazil'), + ('BN', 'Brunei Darussalam'), + ('BG', 'Bulgaria'), + ('BF', 'Burkina Faso'), + ('BI', 'Burundi'), + ('KH', 'Cambodia'), + ('CM', 'Cameroon'), + ('CA', 'Canada'), + ('CV', 'Cape Verde'), + ('KY', 'Cayman Islands'), + ('CF', 'Central African Rep'), + ('TD', 'Chad'), + ('CL', 'Chile'), + ('CN', 'China'), + ('CX', 'Christmas Island'), + ('CC', 'Cocos Islands'), + ('CO', 'Colombia'), + ('KM', 'Comoros'), + ('CG', 'Congo'), + ('CK', 'Cook Islands'), + ('CR', 'Costa Rica'), + ('CI', 'Cote D`ivoire'), + ('HR', 'Croatia'), + ('CU', 'Cuba'), + ('CY', 'Cyprus'), + ('CZ', 'Czech Republic'), + ('DK', 'Denmark'), + ('DJ', 'Djibouti'), + ('DM', 'Dominica'), + ('DO', 'Dominican Republic'), + ('TP', 'East Timor'), + ('EC', 'Ecuador'), + ('EG', 'Egypt'), + ('SV', 'El Salvador'), + ('GQ', 'Equatorial Guinea'), + ('ER', 'Eritrea'), + ('EE', 'Estonia'), + ('ET', 'Ethiopia'), + ('FK', 'Falkland Islands (Malvinas)'), + ('FO', 'Faroe Islands'), + ('FJ', 'Fiji'), + ('FI', 'Finland'), + ('FR', 'France'), + ('GF', 'French Guiana'), + ('PF', 'French Polynesia'), + ('TF', 'French S. Territories'), + ('GA', 'Gabon'), + ('GM', 'Gambia'), + ('GE', 'Georgia'), + ('DE', 'Germany'), + ('GH', 'Ghana'), + ('GI', 'Gibraltar'), + ('GR', 'Greece'), + ('GL', 'Greenland'), + ('GD', 'Grenada'), + ('GP', 'Guadeloupe'), + ('GU', 'Guam'), + ('GT', 'Guatemala'), + ('GN', 'Guinea'), + ('GW', 'Guinea-bissau'), + ('GY', 'Guyana'), + ('HT', 'Haiti'), + ('HN', 'Honduras'), + ('HK', 'Hong Kong'), + ('HU', 'Hungary'), + ('IS', 'Iceland'), + ('IN', 'India'), + ('ID', 'Indonesia'), + ('IR', 'Iran'), + ('IQ', 'Iraq'), + ('IE', 'Ireland'), + ('IL', 'Israel'), + ('IT', 'Italy'), + ('JM', 'Jamaica'), + ('JP', 'Japan'), + ('JO', 'Jordan'), + ('KZ', 'Kazakhstan'), + ('KE', 'Kenya'), + ('KI', 'Kiribati'), + ('KP', 'Korea (North)'), + ('KR', 'Korea (South)'), + ('KW', 'Kuwait'), + ('KG', 'Kyrgyzstan'), + ('LA', 'Laos'), + ('LV', 'Latvia'), + ('LB', 'Lebanon'), + ('LS', 'Lesotho'), + ('LR', 'Liberia'), + ('LY', 'Libya'), + ('LI', 'Liechtenstein'), + ('LT', 'Lithuania'), + ('LU', 'Luxembourg'), + ('MO', 'Macau'), + ('MK', 'Macedonia'), + ('MG', 'Madagascar'), + ('MW', 'Malawi'), + ('MY', 'Malaysia'), + ('MV', 'Maldives'), + ('ML', 'Mali'), + ('MT', 'Malta'), + ('MH', 'Marshall Islands'), + ('MQ', 'Martinique'), + ('MR', 'Mauritania'), + ('MU', 'Mauritius'), + ('YT', 'Mayotte'), + ('MX', 'Mexico'), + ('FM', 'Micronesia'), + ('MD', 'Moldova'), + ('MC', 'Monaco'), + ('MN', 'Mongolia'), + ('MS', 'Montserrat'), + ('MA', 'Morocco'), + ('MZ', 'Mozambique'), + ('MM', 'Myanmar'), + ('NA', 'Namibia'), + ('NR', 'Nauru'), + ('NP', 'Nepal'), + ('NL', 'Netherlands'), + ('AN', 'Netherlands Antilles'), + ('NC', 'New Caledonia'), + ('NZ', 'New Zealand'), + ('NI', 'Nicaragua'), + ('NE', 'Niger'), + ('NG', 'Nigeria'), + ('NU', 'Niue'), + ('NF', 'Norfolk Island'), + ('MP', 'Northern Mariana Islands'), + ('NO', 'Norway'), + ('OM', 'Oman'), + ('PK', 'Pakistan'), + ('PW', 'Palau'), + ('PA', 'Panama'), + ('PG', 'Papua New Guinea'), + ('PY', 'Paraguay'), + ('PE', 'Peru'), + ('PH', 'Philippines'), + ('PN', 'Pitcairn'), + ('PL', 'Poland'), + ('PT', 'Portugal'), + ('PR', 'Puerto Rico'), + ('QA', 'Qatar'), + ('RE', 'Reunion'), + ('RO', 'Romania'), + ('RU', 'Russian Federation'), + ('RW', 'Rwanda'), + ('KN', 'Saint Kitts And Nevis'), + ('LC', 'Saint Lucia'), + ('VC', 'St Vincent/Grenadines'), + ('WS', 'Samoa'), + ('SM', 'San Marino'), + ('ST', 'Sao Tome'), + ('SA', 'Saudi Arabia'), + ('SN', 'Senegal'), + ('SC', 'Seychelles'), + ('SL', 'Sierra Leone'), + ('SG', 'Singapore'), + ('SK', 'Slovakia'), + ('SI', 'Slovenia'), + ('SB', 'Solomon Islands'), + ('SO', 'Somalia'), + ('ZA', 'South Africa'), + ('ES', 'Spain'), + ('LK', 'Sri Lanka'), + ('SH', 'St. Helena'), + ('PM', 'St.Pierre'), + ('SD', 'Sudan'), + ('SR', 'Suriname'), + ('SZ', 'Swaziland'), + ('SE', 'Sweden'), + ('CH', 'Switzerland'), + ('SY', 'Syrian Arab Republic'), + ('TW', 'Taiwan'), + ('TJ', 'Tajikistan'), + ('TZ', 'Tanzania'), + ('TH', 'Thailand'), + ('TG', 'Togo'), + ('TK', 'Tokelau'), + ('TO', 'Tonga'), + ('TT', 'Trinidad And Tobago'), + ('TN', 'Tunisia'), + ('TR', 'Turkey'), + ('TM', 'Turkmenistan'), + ('TV', 'Tuvalu'), + ('UG', 'Uganda'), + ('UA', 'Ukraine'), + ('AE', 'United Arab Emirates'), + ('UK', 'United Kingdom'), + ('US', 'United States'), + ('UY', 'Uruguay'), + ('UZ', 'Uzbekistan'), + ('VU', 'Vanuatu'), + ('VA', 'Vatican City State'), + ('VE', 'Venezuela'), + ('VN', 'Viet Nam'), + ('VG', 'Virgin Islands (British)'), + ('VI', 'Virgin Islands (U.S.)'), + ('EH', 'Western Sahara'), + ('YE', 'Yemen'), + ('YU', 'Yugoslavia'), + ('ZR', 'Zaire'), + ('ZM', 'Zambia'), + ('ZW', 'Zimbabwe') ] def getLanguageDict(): - return lang_dict + return lang_dict def getLanguageFromISO( iso ): - if iso == None: - return None - else: - return lang_dict[ iso ] - - - - - - - - - - + if iso == None: + return None + else: + return lang_dict[ iso ] From adb7cffa640aae166fab0384853d4279352e6ae0 Mon Sep 17 00:00:00 2001 From: Iris Wildthyme <46726098+wildthyme@users.noreply.github.com> Date: Tue, 2 Apr 2019 18:27:24 -0400 Subject: [PATCH 08/22] fixed a python3 issue and made default image not required when parsing a comic file --- comicarchive.py | 2 +- genericmetadata.py | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/comicarchive.py b/comicarchive.py index 8d1a170..8ffe6d4 100755 --- a/comicarchive.py +++ b/comicarchive.py @@ -675,7 +675,7 @@ class ComicArchive: self.archive_type = self.ArchiveType.Pdf self.archiver = PdfArchiver(self.path) - if ComicArchive.logo_data is None: + if ComicArchive.logo_data is None and self.default_image_path: #fname = ComicTaggerSettings.getGraphic('nocover.png') fname = self.default_image_path with open(fname, 'rb') as fd: diff --git a/genericmetadata.py b/genericmetadata.py index 05fa627..de1b3fa 100755 --- a/genericmetadata.py +++ b/genericmetadata.py @@ -174,7 +174,8 @@ class GenericMetadata: def overlayCredits(self, new_credits): for c in new_credits: - if c.has_key('primary') and c['primary']: + if 'primary' in c: + # if c.has_key('primary') and c['primary']: primary = True else: primary = False @@ -294,7 +295,8 @@ class GenericMetadata: for c in self.credits: primary = "" - if c.has_key('primary') and c['primary']: + if 'primary' in c: + # if c.has_key('primary') and c['primary']: primary = " [P]" add_string("credit", c['role'] + ": " + c['person'] + primary) From 710771c6667aff718755e18193173c8eba8cc0e2 Mon Sep 17 00:00:00 2001 From: Iris Wildthyme <46726098+wildthyme@users.noreply.github.com> Date: Tue, 2 Apr 2019 20:33:58 -0400 Subject: [PATCH 09/22] license was missing - added from ComicStreamer. restructured and packaged module --- LICENSE | 202 ++++++++++++++++++ comicapi.egg-info/PKG-INFO | 11 + comicapi.egg-info/SOURCES.txt | 14 ++ comicapi.egg-info/dependency_links.txt | 1 + comicapi.egg-info/requires.txt | 3 + comicapi.egg-info/top_level.txt | 1 + .../UnRAR2}/UnRARDLL/license.txt | 0 .../UnRAR2}/UnRARDLL/unrar.dll | Bin {UnRAR2 => comicapi/UnRAR2}/UnRARDLL/unrar.h | 0 .../UnRAR2}/UnRARDLL/unrar.lib | Bin .../UnRAR2}/UnRARDLL/unrardll.txt | 0 .../UnRAR2}/UnRARDLL/whatsnew.txt | 0 .../UnRAR2}/UnRARDLL/x64/readme.txt | 0 .../UnRAR2}/UnRARDLL/x64/unrar64.dll | Bin .../UnRAR2}/UnRARDLL/x64/unrar64.lib | Bin {UnRAR2 => comicapi/UnRAR2}/__init__.py | 0 {UnRAR2 => comicapi/UnRAR2}/rar_exceptions.py | 0 {UnRAR2 => comicapi/UnRAR2}/test_UnRAR2.py | 0 {UnRAR2 => comicapi/UnRAR2}/unix.py | 0 {UnRAR2 => comicapi/UnRAR2}/windows.py | 0 __init__.py => comicapi/__init__.py | 0 comet.py => comicapi/comet.py | 0 comicarchive.py => comicapi/comicarchive.py | 0 comicbookinfo.py => comicapi/comicbookinfo.py | 0 comicinfoxml.py => comicapi/comicinfoxml.py | 0 .../filenameparser.py | 0 .../genericmetadata.py | 0 issuestring.py => comicapi/issuestring.py | 0 utils.py => comicapi/utils.py | 0 setup.py | 12 ++ 30 files changed, 244 insertions(+) create mode 100644 LICENSE create mode 100644 comicapi.egg-info/PKG-INFO create mode 100644 comicapi.egg-info/SOURCES.txt create mode 100644 comicapi.egg-info/dependency_links.txt create mode 100644 comicapi.egg-info/requires.txt create mode 100644 comicapi.egg-info/top_level.txt rename {UnRAR2 => comicapi/UnRAR2}/UnRARDLL/license.txt (100%) rename {UnRAR2 => comicapi/UnRAR2}/UnRARDLL/unrar.dll (100%) rename {UnRAR2 => comicapi/UnRAR2}/UnRARDLL/unrar.h (100%) rename {UnRAR2 => comicapi/UnRAR2}/UnRARDLL/unrar.lib (100%) rename {UnRAR2 => comicapi/UnRAR2}/UnRARDLL/unrardll.txt (100%) rename {UnRAR2 => comicapi/UnRAR2}/UnRARDLL/whatsnew.txt (100%) rename {UnRAR2 => comicapi/UnRAR2}/UnRARDLL/x64/readme.txt (100%) rename {UnRAR2 => comicapi/UnRAR2}/UnRARDLL/x64/unrar64.dll (100%) rename {UnRAR2 => comicapi/UnRAR2}/UnRARDLL/x64/unrar64.lib (100%) rename {UnRAR2 => comicapi/UnRAR2}/__init__.py (100%) rename {UnRAR2 => comicapi/UnRAR2}/rar_exceptions.py (100%) rename {UnRAR2 => comicapi/UnRAR2}/test_UnRAR2.py (100%) rename {UnRAR2 => comicapi/UnRAR2}/unix.py (100%) rename {UnRAR2 => comicapi/UnRAR2}/windows.py (100%) rename __init__.py => comicapi/__init__.py (100%) rename comet.py => comicapi/comet.py (100%) rename comicarchive.py => comicapi/comicarchive.py (100%) rename comicbookinfo.py => comicapi/comicbookinfo.py (100%) rename comicinfoxml.py => comicapi/comicinfoxml.py (100%) rename filenameparser.py => comicapi/filenameparser.py (100%) rename genericmetadata.py => comicapi/genericmetadata.py (100%) rename issuestring.py => comicapi/issuestring.py (100%) rename utils.py => comicapi/utils.py (100%) create mode 100644 setup.py diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..829fe1a --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/comicapi.egg-info/PKG-INFO b/comicapi.egg-info/PKG-INFO new file mode 100644 index 0000000..ad34ad2 --- /dev/null +++ b/comicapi.egg-info/PKG-INFO @@ -0,0 +1,11 @@ +Metadata-Version: 1.2 +Name: comicapi +Version: 2.0 +Summary: Comic archive (cbr/cbz) and metadata utilities. Extracted from the comictagger project. +Home-page: https://github.com/wildthyme/comicapi +Author: Iris W +License: UNKNOWN +Description: UNKNOWN +Platform: UNKNOWN +Classifier: License :: OSI Approved :: Apache Software License 2.0 (Apache-2.0) +Requires-Python: >=3.6.0 diff --git a/comicapi.egg-info/SOURCES.txt b/comicapi.egg-info/SOURCES.txt new file mode 100644 index 0000000..dc5ed8e --- /dev/null +++ b/comicapi.egg-info/SOURCES.txt @@ -0,0 +1,14 @@ +comicapi/__init__.py +comicapi/comet.py +comicapi/comicarchive.py +comicapi/comicbookinfo.py +comicapi/comicinfoxml.py +comicapi/filenameparser.py +comicapi/genericmetadata.py +comicapi/issuestring.py +comicapi/utils.py +comicapi.egg-info/PKG-INFO +comicapi.egg-info/SOURCES.txt +comicapi.egg-info/dependency_links.txt +comicapi.egg-info/requires.txt +comicapi.egg-info/top_level.txt \ No newline at end of file diff --git a/comicapi.egg-info/dependency_links.txt b/comicapi.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/comicapi.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/comicapi.egg-info/requires.txt b/comicapi.egg-info/requires.txt new file mode 100644 index 0000000..fc56aba --- /dev/null +++ b/comicapi.egg-info/requires.txt @@ -0,0 +1,3 @@ +natsort==3.5.2 +pypdf2==1.24 +unrar==0.3 diff --git a/comicapi.egg-info/top_level.txt b/comicapi.egg-info/top_level.txt new file mode 100644 index 0000000..f5de857 --- /dev/null +++ b/comicapi.egg-info/top_level.txt @@ -0,0 +1 @@ +comicapi diff --git a/UnRAR2/UnRARDLL/license.txt b/comicapi/UnRAR2/UnRARDLL/license.txt similarity index 100% rename from UnRAR2/UnRARDLL/license.txt rename to comicapi/UnRAR2/UnRARDLL/license.txt diff --git a/UnRAR2/UnRARDLL/unrar.dll b/comicapi/UnRAR2/UnRARDLL/unrar.dll similarity index 100% rename from UnRAR2/UnRARDLL/unrar.dll rename to comicapi/UnRAR2/UnRARDLL/unrar.dll diff --git a/UnRAR2/UnRARDLL/unrar.h b/comicapi/UnRAR2/UnRARDLL/unrar.h similarity index 100% rename from UnRAR2/UnRARDLL/unrar.h rename to comicapi/UnRAR2/UnRARDLL/unrar.h diff --git a/UnRAR2/UnRARDLL/unrar.lib b/comicapi/UnRAR2/UnRARDLL/unrar.lib similarity index 100% rename from UnRAR2/UnRARDLL/unrar.lib rename to comicapi/UnRAR2/UnRARDLL/unrar.lib diff --git a/UnRAR2/UnRARDLL/unrardll.txt b/comicapi/UnRAR2/UnRARDLL/unrardll.txt similarity index 100% rename from UnRAR2/UnRARDLL/unrardll.txt rename to comicapi/UnRAR2/UnRARDLL/unrardll.txt diff --git a/UnRAR2/UnRARDLL/whatsnew.txt b/comicapi/UnRAR2/UnRARDLL/whatsnew.txt similarity index 100% rename from UnRAR2/UnRARDLL/whatsnew.txt rename to comicapi/UnRAR2/UnRARDLL/whatsnew.txt diff --git a/UnRAR2/UnRARDLL/x64/readme.txt b/comicapi/UnRAR2/UnRARDLL/x64/readme.txt similarity index 100% rename from UnRAR2/UnRARDLL/x64/readme.txt rename to comicapi/UnRAR2/UnRARDLL/x64/readme.txt diff --git a/UnRAR2/UnRARDLL/x64/unrar64.dll b/comicapi/UnRAR2/UnRARDLL/x64/unrar64.dll similarity index 100% rename from UnRAR2/UnRARDLL/x64/unrar64.dll rename to comicapi/UnRAR2/UnRARDLL/x64/unrar64.dll diff --git a/UnRAR2/UnRARDLL/x64/unrar64.lib b/comicapi/UnRAR2/UnRARDLL/x64/unrar64.lib similarity index 100% rename from UnRAR2/UnRARDLL/x64/unrar64.lib rename to comicapi/UnRAR2/UnRARDLL/x64/unrar64.lib diff --git a/UnRAR2/__init__.py b/comicapi/UnRAR2/__init__.py similarity index 100% rename from UnRAR2/__init__.py rename to comicapi/UnRAR2/__init__.py diff --git a/UnRAR2/rar_exceptions.py b/comicapi/UnRAR2/rar_exceptions.py similarity index 100% rename from UnRAR2/rar_exceptions.py rename to comicapi/UnRAR2/rar_exceptions.py diff --git a/UnRAR2/test_UnRAR2.py b/comicapi/UnRAR2/test_UnRAR2.py similarity index 100% rename from UnRAR2/test_UnRAR2.py rename to comicapi/UnRAR2/test_UnRAR2.py diff --git a/UnRAR2/unix.py b/comicapi/UnRAR2/unix.py similarity index 100% rename from UnRAR2/unix.py rename to comicapi/UnRAR2/unix.py diff --git a/UnRAR2/windows.py b/comicapi/UnRAR2/windows.py similarity index 100% rename from UnRAR2/windows.py rename to comicapi/UnRAR2/windows.py diff --git a/__init__.py b/comicapi/__init__.py similarity index 100% rename from __init__.py rename to comicapi/__init__.py diff --git a/comet.py b/comicapi/comet.py similarity index 100% rename from comet.py rename to comicapi/comet.py diff --git a/comicarchive.py b/comicapi/comicarchive.py similarity index 100% rename from comicarchive.py rename to comicapi/comicarchive.py diff --git a/comicbookinfo.py b/comicapi/comicbookinfo.py similarity index 100% rename from comicbookinfo.py rename to comicapi/comicbookinfo.py diff --git a/comicinfoxml.py b/comicapi/comicinfoxml.py similarity index 100% rename from comicinfoxml.py rename to comicapi/comicinfoxml.py diff --git a/filenameparser.py b/comicapi/filenameparser.py similarity index 100% rename from filenameparser.py rename to comicapi/filenameparser.py diff --git a/genericmetadata.py b/comicapi/genericmetadata.py similarity index 100% rename from genericmetadata.py rename to comicapi/genericmetadata.py diff --git a/issuestring.py b/comicapi/issuestring.py similarity index 100% rename from issuestring.py rename to comicapi/issuestring.py diff --git a/utils.py b/comicapi/utils.py similarity index 100% rename from utils.py rename to comicapi/utils.py diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..7a08c07 --- /dev/null +++ b/setup.py @@ -0,0 +1,12 @@ +from setuptools import setup +setup( + name = 'comicapi', + version = '2.0', + description = 'Comic archive (cbr/cbz) and metadata utilities. Extracted from the comictagger project.', + author = 'Iris W', + packages = ['comicapi'], + install_requires = ['natsort==3.5.2', 'pypdf2==1.24', 'unrar==0.3'], + python_requires = '>=3.6.0', + url = 'https://github.com/wildthyme/comicapi', + classifiers = ['License :: OSI Approved :: Apache Software License 2.0 (Apache-2.0)'] +) From c57bda958e56cf1ae23bec1cde974b4c424b2eb4 Mon Sep 17 00:00:00 2001 From: Iris Wildthyme <46726098+wildthyme@users.noreply.github.com> Date: Tue, 2 Apr 2019 20:51:29 -0400 Subject: [PATCH 10/22] bumped pypdf2 version requirement --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 7a08c07..226d0fd 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setup( description = 'Comic archive (cbr/cbz) and metadata utilities. Extracted from the comictagger project.', author = 'Iris W', packages = ['comicapi'], - install_requires = ['natsort==3.5.2', 'pypdf2==1.24', 'unrar==0.3'], + install_requires = ['natsort==3.5.2', 'pypdf2==1.26', 'unrar==0.3'], python_requires = '>=3.6.0', url = 'https://github.com/wildthyme/comicapi', classifiers = ['License :: OSI Approved :: Apache Software License 2.0 (Apache-2.0)'] From b1b6931de097a7e6242481a1a8c56e6559f63626 Mon Sep 17 00:00:00 2001 From: Iris W <46726098+wildthyme@users.noreply.github.com> Date: Wed, 3 Apr 2019 14:58:04 -0400 Subject: [PATCH 11/22] removed unused UnRAR2 and added gitignore --- .gitignore | 124 +++++ comicapi.egg-info/PKG-INFO | 11 - comicapi.egg-info/SOURCES.txt | 14 - comicapi.egg-info/dependency_links.txt | 1 - comicapi.egg-info/requires.txt | 3 - comicapi.egg-info/top_level.txt | 1 - comicapi/UnRAR2/UnRARDLL/license.txt | 18 - comicapi/UnRAR2/UnRARDLL/unrar.dll | Bin 165376 -> 0 bytes comicapi/UnRAR2/UnRARDLL/unrar.h | 140 ------ comicapi/UnRAR2/UnRARDLL/unrar.lib | Bin 4114 -> 0 bytes comicapi/UnRAR2/UnRARDLL/unrardll.txt | 606 ----------------------- comicapi/UnRAR2/UnRARDLL/whatsnew.txt | 80 --- comicapi/UnRAR2/UnRARDLL/x64/readme.txt | 1 - comicapi/UnRAR2/UnRARDLL/x64/unrar64.dll | Bin 191488 -> 0 bytes comicapi/UnRAR2/UnRARDLL/x64/unrar64.lib | Bin 3972 -> 0 bytes comicapi/UnRAR2/__init__.py | 181 ------- comicapi/UnRAR2/rar_exceptions.py | 43 -- comicapi/UnRAR2/test_UnRAR2.py | 136 ----- comicapi/UnRAR2/unix.py | 226 --------- comicapi/UnRAR2/windows.py | 342 ------------- 20 files changed, 124 insertions(+), 1803 deletions(-) create mode 100644 .gitignore delete mode 100644 comicapi.egg-info/PKG-INFO delete mode 100644 comicapi.egg-info/SOURCES.txt delete mode 100644 comicapi.egg-info/dependency_links.txt delete mode 100644 comicapi.egg-info/requires.txt delete mode 100644 comicapi.egg-info/top_level.txt delete mode 100644 comicapi/UnRAR2/UnRARDLL/license.txt delete mode 100644 comicapi/UnRAR2/UnRARDLL/unrar.dll delete mode 100644 comicapi/UnRAR2/UnRARDLL/unrar.h delete mode 100644 comicapi/UnRAR2/UnRARDLL/unrar.lib delete mode 100644 comicapi/UnRAR2/UnRARDLL/unrardll.txt delete mode 100644 comicapi/UnRAR2/UnRARDLL/whatsnew.txt delete mode 100644 comicapi/UnRAR2/UnRARDLL/x64/readme.txt delete mode 100644 comicapi/UnRAR2/UnRARDLL/x64/unrar64.dll delete mode 100644 comicapi/UnRAR2/UnRARDLL/x64/unrar64.lib delete mode 100755 comicapi/UnRAR2/__init__.py delete mode 100755 comicapi/UnRAR2/rar_exceptions.py delete mode 100755 comicapi/UnRAR2/test_UnRAR2.py delete mode 100755 comicapi/UnRAR2/unix.py delete mode 100755 comicapi/UnRAR2/windows.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5198972 --- /dev/null +++ b/.gitignore @@ -0,0 +1,124 @@ +*.egg-info +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don’t work, or not +# install all needed dependencies. +#Pipfile.lock + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ diff --git a/comicapi.egg-info/PKG-INFO b/comicapi.egg-info/PKG-INFO deleted file mode 100644 index ad34ad2..0000000 --- a/comicapi.egg-info/PKG-INFO +++ /dev/null @@ -1,11 +0,0 @@ -Metadata-Version: 1.2 -Name: comicapi -Version: 2.0 -Summary: Comic archive (cbr/cbz) and metadata utilities. Extracted from the comictagger project. -Home-page: https://github.com/wildthyme/comicapi -Author: Iris W -License: UNKNOWN -Description: UNKNOWN -Platform: UNKNOWN -Classifier: License :: OSI Approved :: Apache Software License 2.0 (Apache-2.0) -Requires-Python: >=3.6.0 diff --git a/comicapi.egg-info/SOURCES.txt b/comicapi.egg-info/SOURCES.txt deleted file mode 100644 index dc5ed8e..0000000 --- a/comicapi.egg-info/SOURCES.txt +++ /dev/null @@ -1,14 +0,0 @@ -comicapi/__init__.py -comicapi/comet.py -comicapi/comicarchive.py -comicapi/comicbookinfo.py -comicapi/comicinfoxml.py -comicapi/filenameparser.py -comicapi/genericmetadata.py -comicapi/issuestring.py -comicapi/utils.py -comicapi.egg-info/PKG-INFO -comicapi.egg-info/SOURCES.txt -comicapi.egg-info/dependency_links.txt -comicapi.egg-info/requires.txt -comicapi.egg-info/top_level.txt \ No newline at end of file diff --git a/comicapi.egg-info/dependency_links.txt b/comicapi.egg-info/dependency_links.txt deleted file mode 100644 index 8b13789..0000000 --- a/comicapi.egg-info/dependency_links.txt +++ /dev/null @@ -1 +0,0 @@ - diff --git a/comicapi.egg-info/requires.txt b/comicapi.egg-info/requires.txt deleted file mode 100644 index fc56aba..0000000 --- a/comicapi.egg-info/requires.txt +++ /dev/null @@ -1,3 +0,0 @@ -natsort==3.5.2 -pypdf2==1.24 -unrar==0.3 diff --git a/comicapi.egg-info/top_level.txt b/comicapi.egg-info/top_level.txt deleted file mode 100644 index f5de857..0000000 --- a/comicapi.egg-info/top_level.txt +++ /dev/null @@ -1 +0,0 @@ -comicapi diff --git a/comicapi/UnRAR2/UnRARDLL/license.txt b/comicapi/UnRAR2/UnRARDLL/license.txt deleted file mode 100644 index 0c1540e..0000000 --- a/comicapi/UnRAR2/UnRARDLL/license.txt +++ /dev/null @@ -1,18 +0,0 @@ - The unrar.dll library is freeware. This means: - - 1. All copyrights to RAR and the unrar.dll are exclusively - owned by the author - Alexander Roshal. - - 2. The unrar.dll library may be used in any software to handle RAR - archives without limitations free of charge. - - 3. THE RAR ARCHIVER AND THE UNRAR.DLL LIBRARY ARE DISTRIBUTED "AS IS". - NO WARRANTY OF ANY KIND IS EXPRESSED OR IMPLIED. YOU USE AT - YOUR OWN RISK. THE AUTHOR WILL NOT BE LIABLE FOR DATA LOSS, - DAMAGES, LOSS OF PROFITS OR ANY OTHER KIND OF LOSS WHILE USING - OR MISUSING THIS SOFTWARE. - - Thank you for your interest in RAR and unrar.dll. - - - Alexander L. Roshal \ No newline at end of file diff --git a/comicapi/UnRAR2/UnRARDLL/unrar.dll b/comicapi/UnRAR2/UnRARDLL/unrar.dll deleted file mode 100644 index 9757bf3d692d8668ce892c4bee814eb5394adf37..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 165376 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 Zpd|w>8EDBsO9omp(2{|c4E$3W_-_?5`fC6H diff --git a/comicapi/UnRAR2/UnRARDLL/unrar.h b/comicapi/UnRAR2/UnRARDLL/unrar.h deleted file mode 100644 index 7643fa7..0000000 --- a/comicapi/UnRAR2/UnRARDLL/unrar.h +++ /dev/null @@ -1,140 +0,0 @@ -#ifndef _UNRAR_DLL_ -#define _UNRAR_DLL_ - -#define ERAR_END_ARCHIVE 10 -#define ERAR_NO_MEMORY 11 -#define ERAR_BAD_DATA 12 -#define ERAR_BAD_ARCHIVE 13 -#define ERAR_UNKNOWN_FORMAT 14 -#define ERAR_EOPEN 15 -#define ERAR_ECREATE 16 -#define ERAR_ECLOSE 17 -#define ERAR_EREAD 18 -#define ERAR_EWRITE 19 -#define ERAR_SMALL_BUF 20 -#define ERAR_UNKNOWN 21 -#define ERAR_MISSING_PASSWORD 22 - -#define RAR_OM_LIST 0 -#define RAR_OM_EXTRACT 1 -#define RAR_OM_LIST_INCSPLIT 2 - -#define RAR_SKIP 0 -#define RAR_TEST 1 -#define RAR_EXTRACT 2 - -#define RAR_VOL_ASK 0 -#define RAR_VOL_NOTIFY 1 - -#define RAR_DLL_VERSION 4 - -#ifdef _UNIX -#define CALLBACK -#define PASCAL -#define LONG long -#define HANDLE void * -#define LPARAM long -#define UINT unsigned int -#endif - -struct RARHeaderData -{ - char ArcName[260]; - char FileName[260]; - unsigned int Flags; - unsigned int PackSize; - unsigned int UnpSize; - unsigned int HostOS; - unsigned int FileCRC; - unsigned int FileTime; - unsigned int UnpVer; - unsigned int Method; - unsigned int FileAttr; - char *CmtBuf; - unsigned int CmtBufSize; - unsigned int CmtSize; - unsigned int CmtState; -}; - - -struct RARHeaderDataEx -{ - char ArcName[1024]; - wchar_t ArcNameW[1024]; - char FileName[1024]; - wchar_t FileNameW[1024]; - unsigned int Flags; - unsigned int PackSize; - unsigned int PackSizeHigh; - unsigned int UnpSize; - unsigned int UnpSizeHigh; - unsigned int HostOS; - unsigned int FileCRC; - unsigned int FileTime; - unsigned int UnpVer; - unsigned int Method; - unsigned int FileAttr; - char *CmtBuf; - unsigned int CmtBufSize; - unsigned int CmtSize; - unsigned int CmtState; - unsigned int Reserved[1024]; -}; - - -struct RAROpenArchiveData -{ - char *ArcName; - unsigned int OpenMode; - unsigned int OpenResult; - char *CmtBuf; - unsigned int CmtBufSize; - unsigned int CmtSize; - unsigned int CmtState; -}; - -struct RAROpenArchiveDataEx -{ - char *ArcName; - wchar_t *ArcNameW; - unsigned int OpenMode; - unsigned int OpenResult; - char *CmtBuf; - unsigned int CmtBufSize; - unsigned int CmtSize; - unsigned int CmtState; - unsigned int Flags; - unsigned int Reserved[32]; -}; - -enum UNRARCALLBACK_MESSAGES { - UCM_CHANGEVOLUME,UCM_PROCESSDATA,UCM_NEEDPASSWORD -}; - -typedef int (CALLBACK *UNRARCALLBACK)(UINT msg,LPARAM UserData,LPARAM P1,LPARAM P2); - -typedef int (PASCAL *CHANGEVOLPROC)(char *ArcName,int Mode); -typedef int (PASCAL *PROCESSDATAPROC)(unsigned char *Addr,int Size); - -#ifdef __cplusplus -extern "C" { -#endif - -HANDLE PASCAL RAROpenArchive(struct RAROpenArchiveData *ArchiveData); -HANDLE PASCAL RAROpenArchiveEx(struct RAROpenArchiveDataEx *ArchiveData); -int PASCAL RARCloseArchive(HANDLE hArcData); -int PASCAL RARReadHeader(HANDLE hArcData,struct RARHeaderData *HeaderData); -int PASCAL RARReadHeaderEx(HANDLE hArcData,struct RARHeaderDataEx *HeaderData); -int PASCAL RARProcessFile(HANDLE hArcData,int Operation,char *DestPath,char *DestName); -int PASCAL RARProcessFileW(HANDLE hArcData,int Operation,wchar_t *DestPath,wchar_t *DestName); -void PASCAL RARSetCallback(HANDLE hArcData,UNRARCALLBACK Callback,LPARAM UserData); -void PASCAL RARSetChangeVolProc(HANDLE hArcData,CHANGEVOLPROC ChangeVolProc); -void PASCAL RARSetProcessDataProc(HANDLE hArcData,PROCESSDATAPROC ProcessDataProc); -void PASCAL RARSetPassword(HANDLE hArcData,char *Password); -int PASCAL RARGetDllVersion(); - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/comicapi/UnRAR2/UnRARDLL/unrar.lib b/comicapi/UnRAR2/UnRARDLL/unrar.lib deleted file mode 100644 index 0f6b3146b8ec5bd83698122653a75bcb1f2caf70..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4114 zcmcInOK%fN5dLhFhn=T@+wLMGkQOc`amG%NR?FnI5XB_X#DU{5*aNW`;>Zr*8-HML zd*Q%x;YaK>;<|@D?B2N`{sm&IW_o&h+GA%dLQ6H>T~*y*cXd^F&DCF=PUG;`!mVPw zES9S))g_~PdnwLe5Z&davS>Xj0QdnIUjrtaK>iId^&z0?62MgW9MDV;@aYrPMAL5r znxQ$EW-URdR1?j;6I7;}XsXU++gtbdcCEU-vAMr)ZSB=}E&Ih$$LYYfcMfW`elcGA z@<3X@cd)Z-i=VOy)#?y-BcN;YV{bWMY1Xgxo+6Zmn>&E6p0KtkH%w4+SmTCs;v|hq5Q}k6xBIHyY3eY03ZFFZ zx+fc+_rUFRTkRurLD_;X896SDC@$tGFxJL_<|ObY4}6#cO4Gn+^7P&e@QLUx^$S#6 zv%o3QI~r6bs*^4S6~-}#3m8Kl1x#QPQ<%mqW{^R4pe*P6b%LAen@j2DWH4cH=}d8! z^o%ndHaMl2_XySiKZu_jIZVRQ_s9EL*FhBJx|L-3_t{EHQd}6?^`Ki%PNfL6xQkm- z4v5&=1)ztX9FY`bs!)xL7(ci^ln5Mfhx#{bsp)wlRL*)ijFpOfIck|4Hvju`dm=+` z2YEY{OsVNUe)07Be$WN(P~-QoBWe@#Yo%6`ZinmiDg@;+ReuwG6#X34CKgVGURAIu zj({&jp&s*16i>5M&r_Un$;(asj7#$q#NpYvusq+pc)!)?w7cymC&e4q&0=k9XWN%* zABt^%AWr}aV^9ds(|62oNeq~c_VZ&}XTJ9bzJ3kCSf2|oEQ@fvCfyUvISe`e#&~(T zkYlh8F(REx#9{sw{)obJ0n4JtRTew+J--3gkftw+ zK8lAdg>=Obk^Q|ACe<1m zhiOKj>CaIFCtE3_O9q#Q_9LNX1zP-xlL(Nllvu-dmg~pzpG}D|GMX{u)Gi1#<;CSh zHv&7?Qyc3?^WXOfPPS57(pXOR$RI}yJTgiSDE*ZHqx<79J5Gq5MOc0!@}1Bo1)7%K zd;?lV{?EoE`zm>VUP05+(QiN;7H@?JQOUz1Fxg7!C6zF>(qj7>?T-H(IN?vsp(PLs F{{t7Z#m)c# diff --git a/comicapi/UnRAR2/UnRARDLL/unrardll.txt b/comicapi/UnRAR2/UnRARDLL/unrardll.txt deleted file mode 100644 index 291c871..0000000 --- a/comicapi/UnRAR2/UnRARDLL/unrardll.txt +++ /dev/null @@ -1,606 +0,0 @@ - - UnRAR.dll Manual - ~~~~~~~~~~~~~~~~ - - UnRAR.dll is a 32-bit Windows dynamic-link library which provides - file extraction from RAR archives. - - - Exported functions - -==================================================================== -HANDLE PASCAL RAROpenArchive(struct RAROpenArchiveData *ArchiveData) -==================================================================== - -Description -~~~~~~~~~~~ - Open RAR archive and allocate memory structures - -Parameters -~~~~~~~~~~ -ArchiveData Points to RAROpenArchiveData structure - -struct RAROpenArchiveData -{ - char *ArcName; - UINT OpenMode; - UINT OpenResult; - char *CmtBuf; - UINT CmtBufSize; - UINT CmtSize; - UINT CmtState; -}; - -Structure fields: - -ArcName - Input parameter which should point to zero terminated string - containing the archive name. - -OpenMode - Input parameter. - - Possible values - - RAR_OM_LIST - Open archive for reading file headers only. - - RAR_OM_EXTRACT - Open archive for testing and extracting files. - - RAR_OM_LIST_INCSPLIT - Open archive for reading file headers only. If you open an archive - in such mode, RARReadHeader[Ex] will return all file headers, - including those with "file continued from previous volume" flag. - In case of RAR_OM_LIST such headers are automatically skipped. - So if you process RAR volumes in RAR_OM_LIST_INCSPLIT mode, you will - get several file header records for same file if file is split between - volumes. For such files only the last file header record will contain - the correct file CRC and if you wish to get the correct packed size, - you need to sum up packed sizes of all parts. - -OpenResult - Output parameter. - - Possible values - - 0 Success - ERAR_NO_MEMORY Not enough memory to initialize data structures - ERAR_BAD_DATA Archive header broken - ERAR_BAD_ARCHIVE File is not valid RAR archive - ERAR_UNKNOWN_FORMAT Unknown encryption used for archive headers - ERAR_EOPEN File open error - -CmtBuf - Input parameter which should point to the buffer for archive - comments. Maximum comment size is limited to 64Kb. Comment text is - zero terminated. If the comment text is larger than the buffer - size, the comment text will be truncated. If CmtBuf is set to - NULL, comments will not be read. - -CmtBufSize - Input parameter which should contain size of buffer for archive - comments. - -CmtSize - Output parameter containing size of comments actually read into the - buffer, cannot exceed CmtBufSize. - -CmtState - Output parameter. - - Possible values - - 0 comments not present - 1 Comments read completely - ERAR_NO_MEMORY Not enough memory to extract comments - ERAR_BAD_DATA Broken comment - ERAR_UNKNOWN_FORMAT Unknown comment format - ERAR_SMALL_BUF Buffer too small, comments not completely read - -Return values -~~~~~~~~~~~~~ - Archive handle or NULL in case of error - - -======================================================================== -HANDLE PASCAL RAROpenArchiveEx(struct RAROpenArchiveDataEx *ArchiveData) -======================================================================== - -Description -~~~~~~~~~~~ - Similar to RAROpenArchive, but uses RAROpenArchiveDataEx structure - allowing to specify Unicode archive name and returning information - about archive flags. - -Parameters -~~~~~~~~~~ -ArchiveData Points to RAROpenArchiveDataEx structure - -struct RAROpenArchiveDataEx -{ - char *ArcName; - wchar_t *ArcNameW; - unsigned int OpenMode; - unsigned int OpenResult; - char *CmtBuf; - unsigned int CmtBufSize; - unsigned int CmtSize; - unsigned int CmtState; - unsigned int Flags; - unsigned int Reserved[32]; -}; - -Structure fields: - -ArcNameW - Input parameter which should point to zero terminated Unicode string - containing the archive name or NULL if Unicode name is not specified. - -Flags - Output parameter. Combination of bit flags. - - Possible values - - 0x0001 - Volume attribute (archive volume) - 0x0002 - Archive comment present - 0x0004 - Archive lock attribute - 0x0008 - Solid attribute (solid archive) - 0x0010 - New volume naming scheme ('volname.partN.rar') - 0x0020 - Authenticity information present - 0x0040 - Recovery record present - 0x0080 - Block headers are encrypted - 0x0100 - First volume (set only by RAR 3.0 and later) - -Reserved[32] - Reserved for future use. Must be zero. - -Information on other structure fields and function return values -is available above, in RAROpenArchive function description. - - -==================================================================== -int PASCAL RARCloseArchive(HANDLE hArcData) -==================================================================== - -Description -~~~~~~~~~~~ - Close RAR archive and release allocated memory. It must be called when - archive processing is finished, even if the archive processing was stopped - due to an error. - -Parameters -~~~~~~~~~~ -hArcData - This parameter should contain the archive handle obtained from the - RAROpenArchive function call. - -Return values -~~~~~~~~~~~~~ - 0 Success - ERAR_ECLOSE Archive close error - - -==================================================================== -int PASCAL RARReadHeader(HANDLE hArcData, - struct RARHeaderData *HeaderData) -==================================================================== - -Description -~~~~~~~~~~~ - Read header of file in archive. - -Parameters -~~~~~~~~~~ -hArcData - This parameter should contain the archive handle obtained from the - RAROpenArchive function call. - -HeaderData - It should point to RARHeaderData structure: - -struct RARHeaderData -{ - char ArcName[260]; - char FileName[260]; - UINT Flags; - UINT PackSize; - UINT UnpSize; - UINT HostOS; - UINT FileCRC; - UINT FileTime; - UINT UnpVer; - UINT Method; - UINT FileAttr; - char *CmtBuf; - UINT CmtBufSize; - UINT CmtSize; - UINT CmtState; -}; - -Structure fields: - -ArcName - Output parameter which contains a zero terminated string of the - current archive name. May be used to determine the current volume - name. - -FileName - Output parameter which contains a zero terminated string of the - file name in OEM (DOS) encoding. - -Flags - Output parameter which contains file flags: - - 0x01 - file continued from previous volume - 0x02 - file continued on next volume - 0x04 - file encrypted with password - 0x08 - file comment present - 0x10 - compression of previous files is used (solid flag) - - bits 7 6 5 - - 0 0 0 - dictionary size 64 Kb - 0 0 1 - dictionary size 128 Kb - 0 1 0 - dictionary size 256 Kb - 0 1 1 - dictionary size 512 Kb - 1 0 0 - dictionary size 1024 Kb - 1 0 1 - dictionary size 2048 KB - 1 1 0 - dictionary size 4096 KB - 1 1 1 - file is directory - - Other bits are reserved. - -PackSize - Output parameter means packed file size or size of the - file part if file was split between volumes. - -UnpSize - Output parameter - unpacked file size. - -HostOS - Output parameter - operating system used for archiving: - - 0 - MS DOS; - 1 - OS/2. - 2 - Win32 - 3 - Unix - -FileCRC - Output parameter which contains unpacked file CRC. In case of file parts - split between volumes only the last part contains the correct CRC - and it is accessible only in RAR_OM_LIST_INCSPLIT listing mode. - -FileTime - Output parameter - contains date and time in standard MS DOS format. - -UnpVer - Output parameter - RAR version needed to extract file. - It is encoded as 10 * Major version + minor version. - -Method - Output parameter - packing method. - -FileAttr - Output parameter - file attributes. - -CmtBuf - File comments support is not implemented in the new DLL version yet. - Now CmtState is always 0. - -/* - * Input parameter which should point to the buffer for file - * comments. Maximum comment size is limited to 64Kb. Comment text is - * a zero terminated string in OEM encoding. If the comment text is - * larger than the buffer size, the comment text will be truncated. - * If CmtBuf is set to NULL, comments will not be read. - */ - -CmtBufSize - Input parameter which should contain size of buffer for archive - comments. - -CmtSize - Output parameter containing size of comments actually read into the - buffer, should not exceed CmtBufSize. - -CmtState - Output parameter. - - Possible values - - 0 Absent comments - 1 Comments read completely - ERAR_NO_MEMORY Not enough memory to extract comments - ERAR_BAD_DATA Broken comment - ERAR_UNKNOWN_FORMAT Unknown comment format - ERAR_SMALL_BUF Buffer too small, comments not completely read - -Return values -~~~~~~~~~~~~~ - - 0 Success - ERAR_END_ARCHIVE End of archive - ERAR_BAD_DATA File header broken - - -==================================================================== -int PASCAL RARReadHeaderEx(HANDLE hArcData, - struct RARHeaderDataEx *HeaderData) -==================================================================== - -Description -~~~~~~~~~~~ - Similar to RARReadHeader, but uses RARHeaderDataEx structure, -containing information about Unicode file names and 64 bit file sizes. - -struct RARHeaderDataEx -{ - char ArcName[1024]; - wchar_t ArcNameW[1024]; - char FileName[1024]; - wchar_t FileNameW[1024]; - unsigned int Flags; - unsigned int PackSize; - unsigned int PackSizeHigh; - unsigned int UnpSize; - unsigned int UnpSizeHigh; - unsigned int HostOS; - unsigned int FileCRC; - unsigned int FileTime; - unsigned int UnpVer; - unsigned int Method; - unsigned int FileAttr; - char *CmtBuf; - unsigned int CmtBufSize; - unsigned int CmtSize; - unsigned int CmtState; - unsigned int Reserved[1024]; -}; - - -==================================================================== -int PASCAL RARProcessFile(HANDLE hArcData, - int Operation, - char *DestPath, - char *DestName) -==================================================================== - -Description -~~~~~~~~~~~ - Performs action and moves the current position in the archive to - the next file. Extract or test the current file from the archive - opened in RAR_OM_EXTRACT mode. If the mode RAR_OM_LIST is set, - then a call to this function will simply skip the archive position - to the next file. - -Parameters -~~~~~~~~~~ -hArcData - This parameter should contain the archive handle obtained from the - RAROpenArchive function call. - -Operation - File operation. - - Possible values - - RAR_SKIP Move to the next file in the archive. If the - archive is solid and RAR_OM_EXTRACT mode was set - when the archive was opened, the current file will - be processed - the operation will be performed - slower than a simple seek. - - RAR_TEST Test the current file and move to the next file in - the archive. If the archive was opened with - RAR_OM_LIST mode, the operation is equal to - RAR_SKIP. - - RAR_EXTRACT Extract the current file and move to the next file. - If the archive was opened with RAR_OM_LIST mode, - the operation is equal to RAR_SKIP. - - -DestPath - This parameter should point to a zero terminated string containing the - destination directory to which to extract files to. If DestPath is equal - to NULL, it means extract to the current directory. This parameter has - meaning only if DestName is NULL. - -DestName - This parameter should point to a string containing the full path and name - to assign to extracted file or it can be NULL to use the default name. - If DestName is defined (not NULL), it overrides both the original file - name saved in the archive and path specigied in DestPath setting. - - Both DestPath and DestName must be in OEM encoding. If necessary, - use CharToOem to convert text to OEM before passing to this function. - -Return values -~~~~~~~~~~~~~ - 0 Success - ERAR_BAD_DATA File CRC error - ERAR_BAD_ARCHIVE Volume is not valid RAR archive - ERAR_UNKNOWN_FORMAT Unknown archive format - ERAR_EOPEN Volume open error - ERAR_ECREATE File create error - ERAR_ECLOSE File close error - ERAR_EREAD Read error - ERAR_EWRITE Write error - - -Note: if you wish to cancel extraction, return -1 when processing - UCM_PROCESSDATA callback message. - - -==================================================================== -int PASCAL RARProcessFileW(HANDLE hArcData, - int Operation, - wchar_t *DestPath, - wchar_t *DestName) -==================================================================== - -Description -~~~~~~~~~~~ - Unicode version of RARProcessFile. It uses Unicode DestPath - and DestName parameters, other parameters and return values - are the same as in RARProcessFile. - - -==================================================================== -void PASCAL RARSetCallback(HANDLE hArcData, - int PASCAL (*CallbackProc)(UINT msg,LPARAM UserData,LPARAM P1,LPARAM P2), - LPARAM UserData); -==================================================================== - -Description -~~~~~~~~~~~ - Set a user-defined callback function to process Unrar events. - -Parameters -~~~~~~~~~~ -hArcData - This parameter should contain the archive handle obtained from the - RAROpenArchive function call. - -CallbackProc - It should point to a user-defined callback function. - - The function will be passed four parameters: - - - msg Type of event. Described below. - - UserData User defined value passed to RARSetCallback. - - P1 and P2 Event dependent parameters. Described below. - - - Possible events - - UCM_CHANGEVOLUME Process volume change. - - P1 Points to the zero terminated name - of the next volume. - - P2 The function call mode: - - RAR_VOL_ASK Required volume is absent. The function should - prompt user and return a positive value - to retry or return -1 value to terminate - operation. The function may also specify a new - volume name, placing it to the address specified - by P1 parameter. - - RAR_VOL_NOTIFY Required volume is successfully opened. - This is a notification call and volume name - modification is not allowed. The function should - return a positive value to continue or -1 - to terminate operation. - - UCM_PROCESSDATA Process unpacked data. It may be used to read - a file while it is being extracted or tested - without actual extracting file to disk. - Return a positive value to continue process - or -1 to cancel the archive operation - - P1 Address pointing to the unpacked data. - Function may refer to the data but must not - change it. - - P2 Size of the unpacked data. It is guaranteed - only that the size will not exceed the maximum - dictionary size (4 Mb in RAR 3.0). - - UCM_NEEDPASSWORD DLL needs a password to process archive. - This message must be processed if you wish - to be able to handle archives with encrypted - file names. It can be also used as replacement - of RARSetPassword function even for usual - encrypted files with non-encrypted names. - - P1 Address pointing to the buffer for a password. - You need to copy a password here. - - P2 Size of the password buffer. - - -UserData - User data passed to callback function. - - Other functions of UnRAR.dll should not be called from the callback - function. - -Return values -~~~~~~~~~~~~~ - None - - - -==================================================================== -void PASCAL RARSetChangeVolProc(HANDLE hArcData, - int PASCAL (*ChangeVolProc)(char *ArcName,int Mode)); -==================================================================== - -Obsoleted, use RARSetCallback instead. - - - -==================================================================== -void PASCAL RARSetProcessDataProc(HANDLE hArcData, - int PASCAL (*ProcessDataProc)(unsigned char *Addr,int Size)) -==================================================================== - -Obsoleted, use RARSetCallback instead. - - -==================================================================== -void PASCAL RARSetPassword(HANDLE hArcData, - char *Password); -==================================================================== - -Description -~~~~~~~~~~~ - Set a password to decrypt files. - -Parameters -~~~~~~~~~~ -hArcData - This parameter should contain the archive handle obtained from the - RAROpenArchive function call. - -Password - It should point to a string containing a zero terminated password. - -Return values -~~~~~~~~~~~~~ - None - - -==================================================================== -void PASCAL RARGetDllVersion(); -==================================================================== - -Description -~~~~~~~~~~~ - Returns API version. - -Parameters -~~~~~~~~~~ - None. - -Return values -~~~~~~~~~~~~~ - Returns an integer value denoting UnRAR.dll API version, which is also -defined in unrar.h as RAR_DLL_VERSION. API version number is incremented -only in case of noticeable changes in UnRAR.dll API. Do not confuse it -with version of UnRAR.dll stored in DLL resources, which is incremented -with every DLL rebuild. - - If RARGetDllVersion() returns a value lower than UnRAR.dll which your -application was designed for, it may indicate that DLL version is too old -and it will fail to provide all necessary functions to your application. - - This function is absent in old versions of UnRAR.dll, so it is safer -to use LoadLibrary and GetProcAddress to access this function. - diff --git a/comicapi/UnRAR2/UnRARDLL/whatsnew.txt b/comicapi/UnRAR2/UnRARDLL/whatsnew.txt deleted file mode 100644 index 84ad72c..0000000 --- a/comicapi/UnRAR2/UnRARDLL/whatsnew.txt +++ /dev/null @@ -1,80 +0,0 @@ -List of unrar.dll API changes. We do not include performance and reliability -improvements into this list, but this library and RAR/UnRAR tools share -the same source code. So the latest version of unrar.dll usually contains -same decompression algorithm changes as the latest UnRAR version. -============================================================================ - --- 18 January 2008 - -all LONG parameters of CallbackProc function were changed -to LPARAM type for 64 bit mode compatibility. - - --- 12 December 2007 - -Added new RAR_OM_LIST_INCSPLIT open mode for function RAROpenArchive. - - --- 14 August 2007 - -Added NoCrypt\unrar_nocrypt.dll without decryption code for those -applications where presence of encryption or decryption code is not -allowed because of legal restrictions. - - --- 14 December 2006 - -Added ERAR_MISSING_PASSWORD error type. This error is returned -if empty password is specified for encrypted file. - - --- 12 June 2003 - -Added RARProcessFileW function, Unicode version of RARProcessFile - - --- 9 August 2002 - -Added RAROpenArchiveEx function allowing to specify Unicode archive -name and get archive flags. - - --- 24 January 2002 - -Added RARReadHeaderEx function allowing to read Unicode file names -and 64 bit file sizes. - - --- 23 January 2002 - -Added ERAR_UNKNOWN error type (it is used for all errors which -do not have special ERAR code yet) and UCM_NEEDPASSWORD callback -message. - -Unrar.dll automatically opens all next volumes not only when extracting, -but also in RAR_OM_LIST mode. - - --- 27 November 2001 - -RARSetChangeVolProc and RARSetProcessDataProc are replaced by -the single callback function installed with RARSetCallback. -Unlike old style callbacks, the new function accepts the user defined -parameter. Unrar.dll still supports RARSetChangeVolProc and -RARSetProcessDataProc for compatibility purposes, but if you write -a new application, better use RARSetCallback. - -File comments support is not implemented in the new DLL version yet. -Now CmtState is always 0. - - --- 13 August 2001 - -Added RARGetDllVersion function, so you may distinguish old unrar.dll, -which used C style callback functions and the new one with PASCAL callbacks. - - --- 10 May 2001 - -Callback functions in RARSetChangeVolProc and RARSetProcessDataProc -use PASCAL style call convention now. diff --git a/comicapi/UnRAR2/UnRARDLL/x64/readme.txt b/comicapi/UnRAR2/UnRARDLL/x64/readme.txt deleted file mode 100644 index 8f3b4e1..0000000 --- a/comicapi/UnRAR2/UnRARDLL/x64/readme.txt +++ /dev/null @@ -1 +0,0 @@ -This is x64 version of unrar.dll. diff --git a/comicapi/UnRAR2/UnRARDLL/x64/unrar64.dll b/comicapi/UnRAR2/UnRARDLL/x64/unrar64.dll deleted file mode 100644 index e17a19e59113c2bd17d1db85f594a1f7c8d4707f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 191488 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 diff --git a/comicapi/UnRAR2/UnRARDLL/x64/unrar64.lib b/comicapi/UnRAR2/UnRARDLL/x64/unrar64.lib deleted file mode 100644 index fd037919ee10c91665fa5f30e9ec6f37ac9e03c7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3972 zcmcImJ5L)y5FTt^e(;c!Dj|{+Ath-DG3N`GqR0l5VDYldtHT_=3$}9D$TkUGIz*8! zb;?lEr=v!Sl){l{Qsh6-lUeWH-K@_T8+R=JFX0GJG)aPil0o?14WT1u~E*0RaD)!9rcy|$9c9u&)3SydpLU0z>YbQ{2D zVZq1O=GT`OvdP3+0y2rr%Dz!dU>FJ?4V<8`x1ViL%H6rcrdFs%yIHeTFtp!L@t7EYzHkb;~448R}+APB=S1U=xncFgNykIP$BP^!vZHz9Cb1ex(;lg;n&NAB^TZ`y9008Qv_ zur8+{+2dPer=4|DqS#t_cWpHdMl&VN91;nKHAG2Lg9OQ*)zQ&OX~1k#rj&Alj@ZGI zlno)g_N)?T*o1DHb&B6~N-MD``;^v?ypXDWTL|<3GLn}G-Pk$U9bXk)NMPfO)e&_Z zMrS4pI;QY=#2UJfjh`rgg~E$<9460%9iHC2V2Z#Mie<;NaR{Gzunu8%uo56nJ%xLt zn0D~1K7w}%*1p5gQXy9^RZ8!x;b)oW;l@3K>vR4ml53`RbY<7T{=&{NJ z8T{VurRFE@w#Af4)VkTX{rZuo&XgbZ#jzyD7qIBL_XE5H6A0N>>1QEQQ3Q7cd^KUsd!5{I~zUy7w?cE7ETsF~fJN4MR` z-rdP%UC3efzGO+NAFDqoRO#VZ@&!b}I?go3eo@Pw zv3G-qqtSb&xN%qfe|O$=y&Y@eo8QK> Y-(qC?Tll6l_w5P#7I3f{o_G}UA258WCIA2c diff --git a/comicapi/UnRAR2/__init__.py b/comicapi/UnRAR2/__init__.py deleted file mode 100755 index 3e043ce..0000000 --- a/comicapi/UnRAR2/__init__.py +++ /dev/null @@ -1,181 +0,0 @@ -# Copyright (c) 2003-2005 Jimmy Retzlaff, 2008 Konstantin Yegupov -# -# Permission is hereby granted, free of charge, to any person obtaining -# a copy of this software and associated documentation files (the -# "Software"), to deal in the Software without restriction, including -# without limitation the rights to use, copy, modify, merge, publish, -# distribute, sublicense, and/or sell copies of the Software, and to -# permit persons to whom the Software is furnished to do so, subject to -# the following conditions: -# -# The above copyright notice and this permission notice shall be -# included in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS -# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN -# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. -""" -pyUnRAR2 is a ctypes based wrapper around the free UnRAR.dll. - -It is an modified version of Jimmy Retzlaff's pyUnRAR - more simple, -stable and foolproof. -Notice that it has INCOMPATIBLE interface. - -It enables reading and unpacking of archives created with the -RAR/WinRAR archivers. There is a low-level interface which is very -similar to the C interface provided by UnRAR. There is also a -higher level interface which makes some common operations easier. -""" - -__version__ = '0.99.3' - -try: - WindowsError - in_windows = True -except NameError: - in_windows = False - -if in_windows: - from comicapi.UnRAR2.windows import RarFileImplementation -else: - from comicapi.UnRAR2.unix import RarFileImplementation - -import fnmatch, time, weakref - - -class RarInfo(object): - """Represents a file header in an archive. Don't instantiate directly. - Use only to obtain information about file. - YOU CANNOT EXTRACT FILE CONTENTS USING THIS OBJECT. - USE METHODS OF RarFile CLASS INSTEAD. - - Properties: - index - index of file within the archive - filename - name of the file in the archive including path (if any) - datetime - file date/time as a struct_time suitable for time.strftime - isdir - True if the file is a directory - size - size in bytes of the uncompressed file - comment - comment associated with the file - - Note - this is not currently intended to be a Python file-like object. - """ - - def __init__(self, rarfile, data): - self.rarfile = weakref.proxy(rarfile) - self.index = data['index'] - self.filename = data['filename'] - self.isdir = data['isdir'] - self.size = data['size'] - self.datetime = data['datetime'] - self.comment = data['comment'] - - def __str__(self): - try: - arcName = self.rarfile.archiveName - except ReferenceError: - arcName = "[ARCHIVE_NO_LONGER_LOADED]" - return '' % (self.filename, arcName) - - -class RarFile(RarFileImplementation): - def __init__(self, archiveName, password=None): - """Instantiate the archive. - - archiveName is the name of the RAR file. - password is used to decrypt the files in the archive. - - Properties: - comment - comment associated with the archive - - >>> print RarFile('test.rar').comment - This is a test. - """ - RarFileImplementation.init(self, archiveName, password) - - def __del__(self): - self.destruct() - - def infoiter(self): - """Iterate over all the files in the archive, generating RarInfos. - - >>> import os - >>> for fileInArchive in RarFile('test.rar').infoiter(): - ... print os.path.split(fileInArchive.filename)[-1], - ... print fileInArchive.isdir, - ... print fileInArchive.size, - ... print fileInArchive.comment, - ... print tuple(fileInArchive.datetime)[0:5], - ... print time.strftime('%a, %d %b %Y %H:%M', fileInArchive.datetime) - test True 0 None (2003, 6, 30, 1, 59) Mon, 30 Jun 2003 01:59 - test.txt False 20 None (2003, 6, 30, 2, 1) Mon, 30 Jun 2003 02:01 - this.py False 1030 None (2002, 2, 8, 16, 47) Fri, 08 Feb 2002 16:47 - """ - for params in RarFileImplementation.infoiter(self): - yield RarInfo(self, params) - - def infolist(self): - """Return a list of RarInfos, descripting the contents of the archive.""" - return list(self.infoiter()) - - def read_files(self, condition='*'): - """Read specific files from archive into memory. - If "condition" is a list of numbers, then return files which have those positions in infolist. - If "condition" is a string, then it is treated as a wildcard for names of files to extract. - If "condition" is a function, it is treated as a callback function, which accepts a RarInfo object - and returns boolean True (extract) or False (skip). - If "condition" is omitted, all files are returned. - - Returns list of tuples (RarInfo info, str contents) - """ - checker = condition2checker(condition) - return RarFileImplementation.read_files(self, checker) - - def extract(self, - condition='*', - path='.', - withSubpath=True, - overwrite=True): - """Extract specific files from archive to disk. - - If "condition" is a list of numbers, then extract files which have those positions in infolist. - If "condition" is a string, then it is treated as a wildcard for names of files to extract. - If "condition" is a function, it is treated as a callback function, which accepts a RarInfo object - and returns either boolean True (extract) or boolean False (skip). - DEPRECATED: If "condition" callback returns string (only supported for Windows) - - that string will be used as a new name to save the file under. - If "condition" is omitted, all files are extracted. - - "path" is a directory to extract to - "withSubpath" flag denotes whether files are extracted with their full path in the archive. - "overwrite" flag denotes whether extracted files will overwrite old ones. Defaults to true. - - Returns list of RarInfos for extracted files.""" - checker = condition2checker(condition) - return RarFileImplementation.extract(self, checker, path, withSubpath, - overwrite) - - -def condition2checker(condition): - """Converts different condition types to callback""" - if type(condition) in [str]: - - def smatcher(info): - return fnmatch.fnmatch(info.filename, condition) - - return smatcher - elif type(condition) in [list, tuple] and type( - condition[0]) in [int]: - - def imatcher(info): - return info.index in condition - - return imatcher - elif callable(condition): - return condition - else: - raise TypeError diff --git a/comicapi/UnRAR2/rar_exceptions.py b/comicapi/UnRAR2/rar_exceptions.py deleted file mode 100755 index 2cf94f7..0000000 --- a/comicapi/UnRAR2/rar_exceptions.py +++ /dev/null @@ -1,43 +0,0 @@ -# Copyright (c) 2003-2005 Jimmy Retzlaff, 2008 Konstantin Yegupov -# -# Permission is hereby granted, free of charge, to any person obtaining -# a copy of this software and associated documentation files (the -# "Software"), to deal in the Software without restriction, including -# without limitation the rights to use, copy, modify, merge, publish, -# distribute, sublicense, and/or sell copies of the Software, and to -# permit persons to whom the Software is furnished to do so, subject to -# the following conditions: -# -# The above copyright notice and this permission notice shall be -# included in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS -# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN -# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. - -# Low level interface - see UnRARDLL\UNRARDLL.TXT - - -class ArchiveHeaderBroken(Exception): - pass - - -class InvalidRARArchive(Exception): - pass - - -class FileOpenError(Exception): - pass - - -class IncorrectRARPassword(Exception): - pass - - -class InvalidRARArchiveUsage(Exception): - pass diff --git a/comicapi/UnRAR2/test_UnRAR2.py b/comicapi/UnRAR2/test_UnRAR2.py deleted file mode 100755 index fcb2597..0000000 --- a/comicapi/UnRAR2/test_UnRAR2.py +++ /dev/null @@ -1,136 +0,0 @@ -import os, sys - -import comicapi.UnRAR2 as UnRAR2 -from comicapi.UnRAR2.rar_exceptions import * - - -def cleanup(dir='test'): - for path, dirs, files in os.walk(dir): - for fn in files: - os.remove(os.path.join(path, fn)) - for dir in dirs: - os.removedirs(os.path.join(path, dir)) - - -# basic test -cleanup() -rarc = UnRAR2.RarFile('test.rar') -rarc.infolist() -assert rarc.comment == "This is a test." -for info in rarc.infoiter(): - saveinfo = info - assert (str(info) == """""") - break -rarc.extract() -assert os.path.exists('test' + os.sep + 'test.txt') -assert os.path.exists('test' + os.sep + 'this.py') -del rarc -assert ( - str(saveinfo) == """""") -cleanup() - -# extract all the files in test.rar -cleanup() -UnRAR2.RarFile('test.rar').extract() -assert os.path.exists('test' + os.sep + 'test.txt') -assert os.path.exists('test' + os.sep + 'this.py') -cleanup() - -# extract all the files in test.rar matching the wildcard *.txt -cleanup() -UnRAR2.RarFile('test.rar').extract('*.txt') -assert os.path.exists('test' + os.sep + 'test.txt') -assert not os.path.exists('test' + os.sep + 'this.py') -cleanup() - -# check the name and size of each file, extracting small ones -cleanup() -archive = UnRAR2.RarFile('test.rar') -assert archive.comment == 'This is a test.' -archive.extract(lambda rarinfo: rarinfo.size <= 1024) -for rarinfo in archive.infoiter(): - if rarinfo.size <= 1024 and not rarinfo.isdir: - assert rarinfo.size == os.stat(rarinfo.filename).st_size -assert open('test' + os.sep + 'test.txt', - 'rt').read() == 'This is only a test.' -assert not os.path.exists('test' + os.sep + 'this.py') -cleanup() - -# extract this.py, overriding it's destination -cleanup('test2') -archive = UnRAR2.RarFile('test.rar') -archive.extract('*.py', 'test2', False) -assert os.path.exists('test2' + os.sep + 'this.py') -cleanup('test2') - -# extract test.txt to memory -cleanup() -archive = UnRAR2.RarFile('test.rar') -entries = UnRAR2.RarFile('test.rar').read_files('*test.txt') -assert len(entries) == 1 -assert entries[0][0].filename.endswith('test.txt') -assert entries[0][1] == 'This is only a test.' - -# extract all the files in test.rar with overwriting -cleanup() -fo = open('test' + os.sep + 'test.txt', "wt") -fo.write("blah") -fo.close() -UnRAR2.RarFile('test.rar').extract('*.txt') -assert open('test' + os.sep + 'test.txt', "rt").read() != "blah" -cleanup() - -# extract all the files in test.rar without overwriting -cleanup() -fo = open('test' + os.sep + 'test.txt', "wt") -fo.write("blahblah") -fo.close() -UnRAR2.RarFile('test.rar').extract('*.txt', overwrite=False) -assert open('test' + os.sep + 'test.txt', "rt").read() == "blahblah" -cleanup() - -# list big file in an archive -list(UnRAR2.RarFile('test_nulls.rar').infoiter()) - -# extract files from an archive with protected files -cleanup() -rarc = UnRAR2.RarFile('test_protected_files.rar', password="protected") -rarc.extract() -assert os.path.exists('test' + os.sep + 'top_secret_xxx_file.txt') -cleanup() -errored = False -try: - UnRAR2.RarFile('test_protected_files.rar', password="proteqted").extract() -except IncorrectRARPassword: - errored = True -assert not os.path.exists('test' + os.sep + 'top_secret_xxx_file.txt') -assert errored -cleanup() - -# extract files from an archive with protected headers -cleanup() -UnRAR2.RarFile('test_protected_headers.rar', password="secret").extract() -assert os.path.exists('test' + os.sep + 'top_secret_xxx_file.txt') -cleanup() -errored = False -try: - UnRAR2.RarFile('test_protected_headers.rar', password="seqret").extract() -except IncorrectRARPassword: - errored = True -assert not os.path.exists('test' + os.sep + 'top_secret_xxx_file.txt') -assert errored -cleanup() - -# make sure docstring examples are working -import doctest -doctest.testmod(UnRAR2) - -# update documentation -import pydoc -pydoc.writedoc(UnRAR2) - -# cleanup -try: - os.remove('__init__.pyc') -except: - pass diff --git a/comicapi/UnRAR2/unix.py b/comicapi/UnRAR2/unix.py deleted file mode 100755 index b962975..0000000 --- a/comicapi/UnRAR2/unix.py +++ /dev/null @@ -1,226 +0,0 @@ -# Copyright (c) 2003-2005 Jimmy Retzlaff, 2008 Konstantin Yegupov -# -# Permission is hereby granted, free of charge, to any person obtaining -# a copy of this software and associated documentation files (the -# "Software"), to deal in the Software without restriction, including -# without limitation the rights to use, copy, modify, merge, publish, -# distribute, sublicense, and/or sell copies of the Software, and to -# permit persons to whom the Software is furnished to do so, subject to -# the following conditions: -# -# The above copyright notice and this permission notice shall be -# included in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS -# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN -# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. - -# Unix version uses unrar command line executable - -import subprocess -import gc - -import os, os.path -import time, re - -from comicapi.UnRAR2.rar_exceptions import * - - -class UnpackerNotInstalled(Exception): - pass - - -rar_executable_cached = None -rar_executable_version = None - - -def call_unrar(params): - "Calls rar/unrar command line executable, returns stdout pipe" - global rar_executable_cached - if rar_executable_cached is None: - for command in ('unrar', 'rar'): - try: - subprocess.Popen([command], stdout=subprocess.PIPE) - rar_executable_cached = command - break - except OSError: - pass - if rar_executable_cached is None: - raise UnpackerNotInstalled("No suitable RAR unpacker installed") - - assert type(params) == list, "params must be list" - args = [rar_executable_cached] + params - try: - gc.disable() # See http://bugs.python.org/issue1336 - return subprocess.Popen( - args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - finally: - gc.enable() - - -class RarFileImplementation(object): - def init(self, archiveName='', password=None): - global rar_executable_version - self.archiveName = archiveName - self.password = password - - stdoutdata, stderrdata = self.call('v', []).communicate() - - for line in stderrdata.splitlines(): - if line.strip().startswith("Cannot open"): - raise FileOpenError - if line.find("CRC failed") >= 0: - raise IncorrectRARPassword - accum = [] - source = iter(stdoutdata.splitlines()) - line = '' - while not (line.startswith('UNRAR')): - line = source.next() - signature = line - # The code below is mighty flaky - # and will probably crash on localized versions of RAR - # but I see no safe way to rewrite it using a CLI tool - if signature.startswith("UNRAR 4"): - rar_executable_version = 4 - while not (line.startswith('Comment:') - or line.startswith('Pathname/Comment')): - if line.strip().endswith('is not RAR archive'): - raise InvalidRARArchive - line = source.next() - while not line.startswith('Pathname/Comment'): - accum.append(line.rstrip('\n')) - line = source.next() - if len(accum): - accum[0] = accum[0][9:] # strip out "Comment:" part - self.comment = '\n'.join(accum[:-1]) - else: - self.comment = None - elif signature.startswith("UNRAR 5"): - rar_executable_version = 5 - line = source.next() - while not line.startswith('Archive:'): - if line.strip().endswith('is not RAR archive'): - raise InvalidRARArchive - accum.append(line.rstrip('\n')) - line = source.next() - if len(accum): - self.comment = '\n'.join(accum[:-1]).strip() - else: - self.comment = None - else: - raise UnpackerNotInstalled( - "Unsupported RAR version, expected 4.x or 5.x, found: " + - signature.split(" ")[1]) - - def escaped_password(self): - return '-' if self.password == None else self.password - - def call(self, cmd, options=[], files=[]): - options2 = options + ['p' + self.escaped_password()] - soptions = ['-' + x for x in options2] - return call_unrar([cmd] + soptions + ['--', self.archiveName] + files) - - def infoiter(self): - - command = "v" if rar_executable_version == 4 else "l" - stdoutdata, stderrdata = self.call(command, ['c-']).communicate() - - for line in stderrdata.splitlines(): - if line.strip().startswith("Cannot open"): - raise FileOpenError - - accum = [] - source = iter(stdoutdata.splitlines()) - line = '' - while not line.startswith('-----------'): - if line.strip().endswith('is not RAR archive'): - raise InvalidRARArchive - if line.startswith("CRC failed") or line.startswith( - "Checksum error"): - raise IncorrectRARPassword - line = source.next() - line = source.next() - i = 0 - re_spaces = re.compile(r"\s+") - if rar_executable_version == 4: - while not line.startswith('-----------'): - accum.append(line) - if len(accum) == 2: - data = {} - data['index'] = i - # asterisks mark password-encrypted files - data['filename'] = accum[0].strip().lstrip( - "*") # asterisks marks password-encrypted files - fields = re_spaces.split(accum[1].strip()) - data['size'] = int(fields[0]) - attr = fields[5] - data['isdir'] = 'd' in attr.lower() - data['datetime'] = time.strptime( - fields[3] + " " + fields[4], '%d-%m-%y %H:%M') - data['comment'] = None - yield data - accum = [] - i += 1 - line = source.next() - elif rar_executable_version == 5: - while not line.startswith('-----------'): - fields = line.strip().lstrip("*").split() - data = {} - data['index'] = i - data['filename'] = " ".join(fields[4:]) - data['size'] = int(fields[1]) - attr = fields[0] - data['isdir'] = 'd' in attr.lower() - data['datetime'] = time.strptime(fields[2] + " " + fields[3], - '%d-%m-%y %H:%M') - data['comment'] = None - yield data - i += 1 - line = source.next() - - def read_files(self, checker): - res = [] - for info in self.infoiter(): - checkres = checker(info) - if checkres == True and not info.isdir: - pipe = self.call('p', ['inul'], [info.filename]).stdout - res.append((info, pipe.read())) - return res - - def extract(self, checker, path, withSubpath, overwrite): - res = [] - command = 'x' - if not withSubpath: - command = 'e' - options = [] - if overwrite: - options.append('o+') - else: - options.append('o-') - if not path.endswith(os.sep): - path += os.sep - names = [] - for info in self.infoiter(): - checkres = checker(info) - if type(checkres) in [str]: - raise NotImplementedError( - "Condition callbacks returning strings are deprecated and only supported in Windows" - ) - if checkres == True and not info.isdir: - names.append(info.filename) - res.append(info) - names.append(path) - proc = self.call(command, options, names) - stdoutdata, stderrdata = proc.communicate() - if stderrdata.find("CRC failed") >= 0 or stderrdata.find( - "Checksum error") >= 0: - raise IncorrectRARPassword - return res - - def destruct(self): - pass diff --git a/comicapi/UnRAR2/windows.py b/comicapi/UnRAR2/windows.py deleted file mode 100755 index b1e71a5..0000000 --- a/comicapi/UnRAR2/windows.py +++ /dev/null @@ -1,342 +0,0 @@ -# Copyright (c) 2003-2005 Jimmy Retzlaff, 2008 Konstantin Yegupov -# -# Permission is hereby granted, free of charge, to any person obtaining -# a copy of this software and associated documentation files (the -# "Software"), to deal in the Software without restriction, including -# without limitation the rights to use, copy, modify, merge, publish, -# distribute, sublicense, and/or sell copies of the Software, and to -# permit persons to whom the Software is furnished to do so, subject to -# the following conditions: -# -# The above copyright notice and this permission notice shall be -# included in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS -# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN -# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. - -# Low level interface - see UnRARDLL\UNRARDLL.TXT - -from __future__ import generators - -import ctypes, ctypes.wintypes -import os, os.path, sys -import Queue -import time - -from comicapi.UnRAR2.rar_exceptions import * - -ERAR_END_ARCHIVE = 10 -ERAR_NO_MEMORY = 11 -ERAR_BAD_DATA = 12 -ERAR_BAD_ARCHIVE = 13 -ERAR_UNKNOWN_FORMAT = 14 -ERAR_EOPEN = 15 -ERAR_ECREATE = 16 -ERAR_ECLOSE = 17 -ERAR_EREAD = 18 -ERAR_EWRITE = 19 -ERAR_SMALL_BUF = 20 -ERAR_UNKNOWN = 21 - -RAR_OM_LIST = 0 -RAR_OM_EXTRACT = 1 - -RAR_SKIP = 0 -RAR_TEST = 1 -RAR_EXTRACT = 2 - -RAR_VOL_ASK = 0 -RAR_VOL_NOTIFY = 1 - -RAR_DLL_VERSION = 3 - -# enum UNRARCALLBACK_MESSAGES -UCM_CHANGEVOLUME = 0 -UCM_PROCESSDATA = 1 -UCM_NEEDPASSWORD = 2 - -architecture_bits = ctypes.sizeof(ctypes.c_voidp) * 8 -dll_name = "unrar.dll" -if architecture_bits == 64: - dll_name = "x64\\unrar64.dll" - -try: - unrar = ctypes.WinDLL( - os.path.join(os.path.split(__file__)[0], 'UnRARDLL', dll_name)) -except WindowsError: - unrar = ctypes.WinDLL(dll_name) - - -class RAROpenArchiveDataEx(ctypes.Structure): - def __init__(self, ArcName=None, ArcNameW=u'', OpenMode=RAR_OM_LIST): - self.CmtBuf = ctypes.c_buffer(64 * 1024) - ctypes.Structure.__init__( - self, - ArcName=ArcName, - ArcNameW=ArcNameW, - OpenMode=OpenMode, - _CmtBuf=ctypes.addressof(self.CmtBuf), - CmtBufSize=ctypes.sizeof(self.CmtBuf)) - - _fields_ = [ - ('ArcName', ctypes.c_char_p), - ('ArcNameW', ctypes.c_wchar_p), - ('OpenMode', ctypes.c_uint), - ('OpenResult', ctypes.c_uint), - ('_CmtBuf', ctypes.c_voidp), - ('CmtBufSize', ctypes.c_uint), - ('CmtSize', ctypes.c_uint), - ('CmtState', ctypes.c_uint), - ('Flags', ctypes.c_uint), - ('Reserved', ctypes.c_uint * 32), - ] - - -class RARHeaderDataEx(ctypes.Structure): - def __init__(self): - self.CmtBuf = ctypes.c_buffer(64 * 1024) - ctypes.Structure.__init__( - self, - _CmtBuf=ctypes.addressof(self.CmtBuf), - CmtBufSize=ctypes.sizeof(self.CmtBuf)) - - _fields_ = [ - ('ArcName', ctypes.c_char * 1024), - ('ArcNameW', ctypes.c_wchar * 1024), - ('FileName', ctypes.c_char * 1024), - ('FileNameW', ctypes.c_wchar * 1024), - ('Flags', ctypes.c_uint), - ('PackSize', ctypes.c_uint), - ('PackSizeHigh', ctypes.c_uint), - ('UnpSize', ctypes.c_uint), - ('UnpSizeHigh', ctypes.c_uint), - ('HostOS', ctypes.c_uint), - ('FileCRC', ctypes.c_uint), - ('FileTime', ctypes.c_uint), - ('UnpVer', ctypes.c_uint), - ('Method', ctypes.c_uint), - ('FileAttr', ctypes.c_uint), - ('_CmtBuf', ctypes.c_voidp), - ('CmtBufSize', ctypes.c_uint), - ('CmtSize', ctypes.c_uint), - ('CmtState', ctypes.c_uint), - ('Reserved', ctypes.c_uint * 1024), - ] - - -def DosDateTimeToTimeTuple(dosDateTime): - """Convert an MS-DOS format date time to a Python time tuple. - """ - dosDate = dosDateTime >> 16 - dosTime = dosDateTime & 0xffff - day = dosDate & 0x1f - month = (dosDate >> 5) & 0xf - year = 1980 + (dosDate >> 9) - second = 2 * (dosTime & 0x1f) - minute = (dosTime >> 5) & 0x3f - hour = dosTime >> 11 - return time.localtime( - time.mktime((year, month, day, hour, minute, second, 0, 1, -1))) - - -def _wrap(restype, function, argtypes): - result = function - result.argtypes = argtypes - result.restype = restype - return result - - -RARGetDllVersion = _wrap(ctypes.c_int, unrar.RARGetDllVersion, []) - -RAROpenArchiveEx = _wrap(ctypes.wintypes.HANDLE, unrar.RAROpenArchiveEx, - [ctypes.POINTER(RAROpenArchiveDataEx)]) - -RARReadHeaderEx = _wrap( - ctypes.c_int, unrar.RARReadHeaderEx, - [ctypes.wintypes.HANDLE, - ctypes.POINTER(RARHeaderDataEx)]) - -_RARSetPassword = _wrap(ctypes.c_int, unrar.RARSetPassword, - [ctypes.wintypes.HANDLE, ctypes.c_char_p]) - - -def RARSetPassword(*args, **kwargs): - _RARSetPassword(*args, **kwargs) - - -RARProcessFile = _wrap( - ctypes.c_int, unrar.RARProcessFile, - [ctypes.wintypes.HANDLE, ctypes.c_int, ctypes.c_char_p, ctypes.c_char_p]) - -RARCloseArchive = _wrap(ctypes.c_int, unrar.RARCloseArchive, - [ctypes.wintypes.HANDLE]) - -UNRARCALLBACK = ctypes.WINFUNCTYPE(ctypes.c_int, ctypes.c_uint, ctypes.c_long, - ctypes.c_long, ctypes.c_long) -RARSetCallback = _wrap(ctypes.c_int, unrar.RARSetCallback, - [ctypes.wintypes.HANDLE, UNRARCALLBACK, ctypes.c_long]) - -RARExceptions = { - ERAR_NO_MEMORY: MemoryError, - ERAR_BAD_DATA: ArchiveHeaderBroken, - ERAR_BAD_ARCHIVE: InvalidRARArchive, - ERAR_EOPEN: FileOpenError, -} - - -class PassiveReader: - """Used for reading files to memory""" - - def __init__(self, usercallback=None): - self.buf = [] - self.ucb = usercallback - - def _callback(self, msg, UserData, P1, P2): - if msg == UCM_PROCESSDATA: - data = (ctypes.c_char * P2).from_address(P1).raw - if self.ucb != None: - self.ucb(data) - else: - self.buf.append(data) - return 1 - - def get_result(self): - return ''.join(self.buf) - - -class RarInfoIterator(object): - def __init__(self, arc): - self.arc = arc - self.index = 0 - self.headerData = RARHeaderDataEx() - self.res = RARReadHeaderEx(self.arc._handle, - ctypes.byref(self.headerData)) - if self.res == ERAR_BAD_DATA: - raise IncorrectRARPassword - self.arc.lockStatus = "locked" - self.arc.needskip = False - - def __iter__(self): - return self - - def next(self): - if self.index > 0: - if self.arc.needskip: - RARProcessFile(self.arc._handle, RAR_SKIP, None, None) - self.res = RARReadHeaderEx(self.arc._handle, - ctypes.byref(self.headerData)) - - if self.res: - raise StopIteration - self.arc.needskip = True - - data = {} - data['index'] = self.index - data['filename'] = self.headerData.FileName - data['datetime'] = DosDateTimeToTimeTuple(self.headerData.FileTime) - data['isdir'] = ((self.headerData.Flags & 0xE0) == 0xE0) - data['size'] = self.headerData.UnpSize + ( - self.headerData.UnpSizeHigh << 32) - if self.headerData.CmtState == 1: - data['comment'] = self.headerData.CmtBuf.value - else: - data['comment'] = None - self.index += 1 - return data - - def __del__(self): - self.arc.lockStatus = "finished" - - -def generate_password_provider(password): - def password_provider_callback(msg, UserData, P1, P2): - if msg == UCM_NEEDPASSWORD and password != None: - (ctypes.c_char * P2).from_address(P1).value = password - return 1 - - return password_provider_callback - - -class RarFileImplementation(object): - def init(self, archiveName = '', password=None): - self.archiveName = archiveName - self.password = password - archiveData = RAROpenArchiveDataEx( - ArcNameW=self.archiveName, OpenMode=RAR_OM_EXTRACT) - self._handle = RAROpenArchiveEx(ctypes.byref(archiveData)) - self.c_callback = UNRARCALLBACK( - generate_password_provider(self.password)) - RARSetCallback(self._handle, self.c_callback, 1) - - if archiveData.OpenResult != 0: - raise RARExceptions[archiveData.OpenResult] - - if archiveData.CmtState == 1: - self.comment = archiveData.CmtBuf.value - else: - self.comment = None - - if password: - RARSetPassword(self._handle, password) - - self.lockStatus = "ready" - - def destruct(self): - if self._handle and RARCloseArchive: - RARCloseArchive(self._handle) - - def make_sure_ready(self): - if self.lockStatus == "locked": - raise InvalidRARArchiveUsage( - "cannot execute infoiter() without finishing previous one") - if self.lockStatus == "finished": - self.destruct() - self.init(self.password) - - def infoiter(self): - self.make_sure_ready() - return RarInfoIterator(self) - - def read_files(self, checker): - res = [] - for info in self.infoiter(): - if checker(info) and not info.isdir: - reader = PassiveReader() - c_callback = UNRARCALLBACK(reader._callback) - RARSetCallback(self._handle, c_callback, 1) - tmpres = RARProcessFile(self._handle, RAR_TEST, None, None) - if tmpres == ERAR_BAD_DATA: - raise IncorrectRARPassword - self.needskip = False - res.append((info, reader.get_result())) - return res - - def extract(self, checker, path, withSubpath, overwrite): - res = [] - for info in self.infoiter(): - checkres = checker(info) - if checkres != False and not info.isdir: - if checkres == True: - fn = info.filename - if not withSubpath: - fn = os.path.split(fn)[-1] - target = os.path.join(path, fn) - else: - target = checkres - raise DeprecationWarning("Condition callbacks returning strings are deprecated and only supported in Windows") - - if overwrite or (not os.path.exists(target)): - tmpres = RARProcessFile(self._handle, RAR_EXTRACT, None, - target) - if tmpres == ERAR_BAD_DATA: - raise IncorrectRARPassword - - self.needskip = False - res.append(info) - return res From 0f36fdd81bd03d6a979bebf03f907a1405d992c4 Mon Sep 17 00:00:00 2001 From: Iris W <46726098+wildthyme@users.noreply.github.com> Date: Wed, 3 Apr 2019 15:32:13 -0400 Subject: [PATCH 12/22] made cbr support optional cos it's a hassle, and added README.md --- README.md | 5 + comicapi/comicarchive.py | 453 ++++++++++++++++++++------------------- setup.py | 5 +- 3 files changed, 238 insertions(+), 225 deletions(-) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..e858949 --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +# History +comicapi originates [here](https://github.com/davide-romanini/comicapi), was integrated into [ComicStreamer](https://github.com/davide-romanini/ComicStreamer), was modified in [this fork](https://github.com/kounch/ComicStreamer), and has now been extracted and packaged by yours truly (Iris W). + +# Installation +you can use pip to install this. cbr support is off by default—you'll need to do `pip install unrar` as well as having libunrar.so available. diff --git a/comicapi/comicarchive.py b/comicapi/comicarchive.py index 8ffe6d4..220406e 100755 --- a/comicapi/comicarchive.py +++ b/comicapi/comicarchive.py @@ -27,50 +27,55 @@ import locale import shutil from natsort import natsorted -from unrar import rarfile -from unrar import unrarlib -import unrar.constants +try: + from unrar import rarfile + from unrar import unrarlib + import unrar.constants + from unrar import constants + rarsupport = True +except ImportError: + rarsupport = False import ctypes import io -from unrar import constants -class OpenableRarFile(rarfile.RarFile): - def open(self, member): - #print "opening %s..." % member - # based on https://github.com/matiasb/python-unrar/pull/4/files - if isinstance(member, rarfile.RarInfo): - member = member.filename - archive = unrarlib.RAROpenArchiveDataEx( - self.filename, mode=constants.RAR_OM_EXTRACT) - handle = self._open(archive) - found, buf = False, [] +if rarsupport: + class OpenableRarFile(rarfile.RarFile): + def open(self, member): + #print "opening %s..." % member + # based on https://github.com/matiasb/python-unrar/pull/4/files + if isinstance(member, rarfile.RarInfo): + member = member.filename + archive = unrarlib.RAROpenArchiveDataEx( + self.filename, mode=constants.RAR_OM_EXTRACT) + handle = self._open(archive) + found, buf = False, [] - def _callback(msg, UserData, P1, P2): - if msg == constants.UCM_PROCESSDATA: - data = (ctypes.c_char * P2).from_address(P1).raw - buf.append(data) - return 1 + def _callback(msg, UserData, P1, P2): + if msg == constants.UCM_PROCESSDATA: + data = (ctypes.c_char * P2).from_address(P1).raw + buf.append(data) + return 1 - c_callback = unrarlib.UNRARCALLBACK(_callback) - unrarlib.RARSetCallback(handle, c_callback, 1) - try: - rarinfo = self._read_header(handle) - while rarinfo is not None: - #print "checking rar archive %s against %s" % (rarinfo.filename, member) - if rarinfo.filename == member: - self._process_current(handle, constants.RAR_TEST) - found = True - else: - self._process_current(handle, constants.RAR_SKIP) + c_callback = unrarlib.UNRARCALLBACK(_callback) + unrarlib.RARSetCallback(handle, c_callback, 1) + try: rarinfo = self._read_header(handle) - except unrarlib.UnrarException: - raise rarfile.BadRarFile("Bad RAR archive data.") - finally: - self._close(handle) - if not found: - raise KeyError('There is no item named %r in the archive' % member) - return b''.join(buf) + while rarinfo is not None: + #print "checking rar archive %s against %s" % (rarinfo.filename, member) + if rarinfo.filename == member: + self._process_current(handle, constants.RAR_TEST) + found = True + else: + self._process_current(handle, constants.RAR_SKIP) + rarinfo = self._read_header(handle) + except unrarlib.UnrarException: + raise rarfile.BadRarFile("Bad RAR archive data.") + finally: + self._close(handle) + if not found: + raise KeyError('There is no item named %r in the archive' % member) + return b''.join(buf) # if platform.system() == "Windows": @@ -293,223 +298,223 @@ class ZipArchiver: #------------------------------------------ # RAR implementation +if rarsupport: + class RarArchiver: -class RarArchiver: + devnull = None - devnull = None + def __init__(self, path, rar_exe_path): + self.path = path + self.rar_exe_path = rar_exe_path - def __init__(self, path, rar_exe_path): - self.path = path - self.rar_exe_path = rar_exe_path + if RarArchiver.devnull is None: + RarArchiver.devnull = open(os.devnull, "w") - if RarArchiver.devnull is None: - RarArchiver.devnull = open(os.devnull, "w") - - # windows only, keeps the cmd.exe from popping up - if platform.system() == "Windows": - # self.startupinfo = subprocess.STARTUPINFO() - # self.startupinfo.dwFlags |= _subprocess.STARTF_USESHOWWINDOW - self.startupinfo = None - else: - self.startupinfo = None - - def __del__(self): - #RarArchiver.devnull.close() - pass - - def getArchiveComment(self): - - rarc = self.getRARObj() - return rarc.comment - - def setArchiveComment(self, comment): - - if self.rar_exe_path is not None: - try: - # write comment to temp file - tmp_fd, tmp_name = tempfile.mkstemp() - f = os.fdopen(tmp_fd, 'w+b') - f.write(comment) - f.close() - - working_dir = os.path.dirname(os.path.abspath(self.path)) - - # use external program to write comment to Rar archive - subprocess.call( - [ - self.rar_exe_path, 'c', '-w' + working_dir, '-c-', - '-z' + tmp_name, self.path - ], - startupinfo=self.startupinfo, - stdout=RarArchiver.devnull) - - if platform.system() == "Darwin": - time.sleep(1) - - os.remove(tmp_name) - except: - return False + # windows only, keeps the cmd.exe from popping up + if platform.system() == "Windows": + # self.startupinfo = subprocess.STARTUPINFO() + # self.startupinfo.dwFlags |= _subprocess.STARTF_USESHOWWINDOW + self.startupinfo = None else: - return True - else: - return False + self.startupinfo = None - def readArchiveFile(self, archive_file): + def __del__(self): + #RarArchiver.devnull.close() + pass - # Make sure to escape brackets, since some funky stuff is going on - # underneath with "fnmatch" - #archive_file = archive_file.replace("[", '[[]') - entries = [] + def getArchiveComment(self): - rarc = self.getRARObj() + rarc = self.getRARObj() + return rarc.comment - tries = 0 - while tries < 7: - try: - tries = tries + 1 - #tmp_folder = tempfile.mkdtemp() - #tmp_file = os.path.join(tmp_folder, archive_file) - #rarc.extract(archive_file, tmp_folder) - data = rarc.open(archive_file) - #data = open(tmp_file).read() - entries = [(rarc.getinfo(archive_file), data)] + def setArchiveComment(self, comment): - #shutil.rmtree(tmp_folder, ignore_errors=True) + if self.rar_exe_path is not None: + try: + # write comment to temp file + tmp_fd, tmp_name = tempfile.mkstemp() + f = os.fdopen(tmp_fd, 'w+b') + f.write(comment) + f.close() - #entries = rarc.read_files( archive_file ) + working_dir = os.path.dirname(os.path.abspath(self.path)) - if entries[0][0].file_size != len(entries[0][1]): - errMsg = u"readArchiveFile(): [file is not expected size: {0} vs {1}] {2}:{3} [attempt # {4}]".format( - entries[0][0].file_size, len(entries[0][1]), self.path, - archive_file, tries) - sys.stderr.buffer.write(bytes(errMsg, "UTF-8")) - continue + # use external program to write comment to Rar archive + subprocess.call( + [ + self.rar_exe_path, 'c', '-w' + working_dir, '-c-', + '-z' + tmp_name, self.path + ], + startupinfo=self.startupinfo, + stdout=RarArchiver.devnull) - except (OSError, IOError) as e: - errMsg = u"readArchiveFile(): [{0}] {1}:{2} attempt#{3}".format( - str(e), self.path, archive_file, tries) - sys.stderr.buffer.write(bytes(errMsg, "UTF-8")) - time.sleep(1) - except Exception as e: - errMsg = u"Unexpected exception in readArchiveFile(): [{0}] for {1}:{2} attempt#{3}".format( - str(e), self.path, archive_file, tries) - sys.stderr.buffer.write(bytes(errMsg, "UTF-8")) - break + if platform.system() == "Darwin": + time.sleep(1) - else: - #Success" - #entries is a list of of tuples: ( rarinfo, filedata) - if tries > 1: - errMsg = u"Attempted read_files() {0} times".format(tries) - sys.stderr.buffer.write(bytes(errMsg, "UTF-8")) - if (len(entries) == 1): - return entries[0][1] + os.remove(tmp_name) + except: + return False else: - raise IOError - - raise IOError - - def writeArchiveFile(self, archive_file, data): - - if self.rar_exe_path is not None: - try: - tmp_folder = tempfile.mkdtemp() - - tmp_file = os.path.join(tmp_folder, archive_file) - - working_dir = os.path.dirname(os.path.abspath(self.path)) - - # TODO: will this break if 'archive_file' is in a subfolder. i.e. "foo/bar.txt" - # will need to create the subfolder above, I guess... - f = open(tmp_file, 'w') - f.write(data) - f.close() - - # use external program to write file to Rar archive - subprocess.call( - [ - self.rar_exe_path, 'a', '-w' + working_dir, '-c-', - '-ep', self.path, tmp_file - ], - startupinfo=self.startupinfo, - stdout=RarArchiver.devnull) - - if platform.system() == "Darwin": - time.sleep(1) - os.remove(tmp_file) - os.rmdir(tmp_folder) - except: + return True + else: return False - else: - return True - else: - return False - def removeArchiveFile(self, archive_file): - if self.rar_exe_path is not None: - try: - # use external program to remove file from Rar archive - subprocess.call( - [self.rar_exe_path, 'd', '-c-', self.path, archive_file], - startupinfo=self.startupinfo, - stdout=RarArchiver.devnull) + def readArchiveFile(self, archive_file): - if platform.system() == "Darwin": + # Make sure to escape brackets, since some funky stuff is going on + # underneath with "fnmatch" + #archive_file = archive_file.replace("[", '[[]') + entries = [] + + rarc = self.getRARObj() + + tries = 0 + while tries < 7: + try: + tries = tries + 1 + #tmp_folder = tempfile.mkdtemp() + #tmp_file = os.path.join(tmp_folder, archive_file) + #rarc.extract(archive_file, tmp_folder) + data = rarc.open(archive_file) + #data = open(tmp_file).read() + entries = [(rarc.getinfo(archive_file), data)] + + #shutil.rmtree(tmp_folder, ignore_errors=True) + + #entries = rarc.read_files( archive_file ) + + if entries[0][0].file_size != len(entries[0][1]): + errMsg = u"readArchiveFile(): [file is not expected size: {0} vs {1}] {2}:{3} [attempt # {4}]".format( + entries[0][0].file_size, len(entries[0][1]), self.path, + archive_file, tries) + sys.stderr.buffer.write(bytes(errMsg, "UTF-8")) + continue + + except (OSError, IOError) as e: + errMsg = u"readArchiveFile(): [{0}] {1}:{2} attempt#{3}".format( + str(e), self.path, archive_file, tries) + sys.stderr.buffer.write(bytes(errMsg, "UTF-8")) time.sleep(1) - except: + except Exception as e: + errMsg = u"Unexpected exception in readArchiveFile(): [{0}] for {1}:{2} attempt#{3}".format( + str(e), self.path, archive_file, tries) + sys.stderr.buffer.write(bytes(errMsg, "UTF-8")) + break + + else: + #Success" + #entries is a list of of tuples: ( rarinfo, filedata) + if tries > 1: + errMsg = u"Attempted read_files() {0} times".format(tries) + sys.stderr.buffer.write(bytes(errMsg, "UTF-8")) + if (len(entries) == 1): + return entries[0][1] + else: + raise IOError + + raise IOError + + def writeArchiveFile(self, archive_file, data): + + if self.rar_exe_path is not None: + try: + tmp_folder = tempfile.mkdtemp() + + tmp_file = os.path.join(tmp_folder, archive_file) + + working_dir = os.path.dirname(os.path.abspath(self.path)) + + # TODO: will this break if 'archive_file' is in a subfolder. i.e. "foo/bar.txt" + # will need to create the subfolder above, I guess... + f = open(tmp_file, 'w') + f.write(data) + f.close() + + # use external program to write file to Rar archive + subprocess.call( + [ + self.rar_exe_path, 'a', '-w' + working_dir, '-c-', + '-ep', self.path, tmp_file + ], + startupinfo=self.startupinfo, + stdout=RarArchiver.devnull) + + if platform.system() == "Darwin": + time.sleep(1) + os.remove(tmp_file) + os.rmdir(tmp_folder) + except: + return False + else: + return True + else: return False + + def removeArchiveFile(self, archive_file): + if self.rar_exe_path is not None: + try: + # use external program to remove file from Rar archive + subprocess.call( + [self.rar_exe_path, 'd', '-c-', self.path, archive_file], + startupinfo=self.startupinfo, + stdout=RarArchiver.devnull) + + if platform.system() == "Darwin": + time.sleep(1) + except: + return False + else: + return True else: - return True - else: - return False + return False - def getArchiveFilenameList(self): + def getArchiveFilenameList(self): - rarc = self.getRARObj() - #namelist = [ item.filename for item in rarc.infolist() ] - #return namelist + rarc = self.getRARObj() + #namelist = [ item.filename for item in rarc.infolist() ] + #return namelist - tries = 0 - while tries < 7: - try: - tries = tries + 1 - #namelist = [ item.filename for item in rarc.infolist() ] - namelist = [] - for item in rarc.infolist(): - if item.file_size != 0: - namelist.append(item.filename) + tries = 0 + while tries < 7: + try: + tries = tries + 1 + #namelist = [ item.filename for item in rarc.infolist() ] + namelist = [] + for item in rarc.infolist(): + if item.file_size != 0: + namelist.append(item.filename) - except (OSError, IOError) as e: - errMsg = u"getArchiveFilenameList(): [{0}] {1} attempt#{2}".format( - str(e), self.path, tries) - sys.stderr.buffer.write(bytes(errMsg, "UTF-8")) - time.sleep(1) + except (OSError, IOError) as e: + errMsg = u"getArchiveFilenameList(): [{0}] {1} attempt#{2}".format( + str(e), self.path, tries) + sys.stderr.buffer.write(bytes(errMsg, "UTF-8")) + time.sleep(1) - else: - #Success" - return namelist + else: + #Success" + return namelist - raise e + raise e - def getRARObj(self): - tries = 0 - while tries < 7: - try: - tries = tries + 1 - #rarc = UnRAR2.RarFile( self.path ) - rarc = OpenableRarFile(self.path) + def getRARObj(self): + tries = 0 + while tries < 7: + try: + tries = tries + 1 + #rarc = UnRAR2.RarFile( self.path ) + rarc = OpenableRarFile(self.path) - except (OSError, IOError) as e: - errMsg = u"getRARObj(): [{0}] {1} attempt#{2}".format( - str(e), self.path, tries) - sys.stderr.buffer.write(bytes(errMsg, "UTF-8")) - time.sleep(1) + except (OSError, IOError) as e: + errMsg = u"getRARObj(): [{0}] {1} attempt#{2}".format( + str(e), self.path, tries) + sys.stderr.buffer.write(bytes(errMsg, "UTF-8")) + time.sleep(1) - else: - #Success" - return rarc + else: + #Success" + return rarc - raise e + raise e #------------------------------------------ @@ -653,7 +658,7 @@ class ComicArchive: self.archive_type = self.ArchiveType.Unknown self.archiver = UnknownArchiver(self.path) - if ext == ".cbr" or ext == ".rar": + if rarsupport and ext == ".cbr" or ext == ".rar": if self.rarTest(): self.archive_type = self.ArchiveType.Rar self.archiver = RarArchiver( diff --git a/setup.py b/setup.py index 226d0fd..06ccec6 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,10 @@ setup( description = 'Comic archive (cbr/cbz) and metadata utilities. Extracted from the comictagger project.', author = 'Iris W', packages = ['comicapi'], - install_requires = ['natsort==3.5.2', 'pypdf2==1.26', 'unrar==0.3'], + install_requires = ['natsort==3.5.2', 'pypdf2==1.26'], + extras_require = { + 'CBR': ['unrar==0.3'] + } python_requires = '>=3.6.0', url = 'https://github.com/wildthyme/comicapi', classifiers = ['License :: OSI Approved :: Apache Software License 2.0 (Apache-2.0)'] From cb279168f9c5cec742b5a05ac8326b9c168a8a91 Mon Sep 17 00:00:00 2001 From: Iris W <46726098+wildthyme@users.noreply.github.com> Date: Tue, 9 Apr 2019 15:18:28 -0400 Subject: [PATCH 13/22] fixed syntax error --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 06ccec6..a183cf8 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ setup( install_requires = ['natsort==3.5.2', 'pypdf2==1.26'], extras_require = { 'CBR': ['unrar==0.3'] - } + }, python_requires = '>=3.6.0', url = 'https://github.com/wildthyme/comicapi', classifiers = ['License :: OSI Approved :: Apache Software License 2.0 (Apache-2.0)'] From 5346716578b2843f54d522f44d01bc8d25001d24 Mon Sep 17 00:00:00 2001 From: Ozzie Isaacs Date: Mon, 15 Jul 2019 20:17:36 +0200 Subject: [PATCH 14/22] changed Minimum python version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index a183cf8..d440a75 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( extras_require = { 'CBR': ['unrar==0.3'] }, - python_requires = '>=3.6.0', + python_requires = '>=2.7.0', url = 'https://github.com/wildthyme/comicapi', classifiers = ['License :: OSI Approved :: Apache Software License 2.0 (Apache-2.0)'] ) From ad8bfe5a1c31db882480433f86db2c5c57634a3f Mon Sep 17 00:00:00 2001 From: Ozzie Isaacs Date: Sun, 19 Jan 2020 17:38:02 +0100 Subject: [PATCH 15/22] Updated dependency requirements --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index d440a75..fad7a56 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setup( description = 'Comic archive (cbr/cbz) and metadata utilities. Extracted from the comictagger project.', author = 'Iris W', packages = ['comicapi'], - install_requires = ['natsort==3.5.2', 'pypdf2==1.26'], + install_requires = ['natsort>=3.5.2', 'pypdf2>=1.26'], extras_require = { 'CBR': ['unrar==0.3'] }, From 15dff9ce4e1ffed29ba4a2feadfcdb6bed00bcad Mon Sep 17 00:00:00 2001 From: Ozzieisaacs Date: Tue, 28 Apr 2020 20:32:10 +0200 Subject: [PATCH 16/22] Changed unrar to rarfile Removed unused pypdf dependency in setup --- comicapi/comicarchive.py | 27 ++++++++++----------------- setup.py | 8 ++++---- 2 files changed, 14 insertions(+), 21 deletions(-) diff --git a/comicapi/comicarchive.py b/comicapi/comicarchive.py index 220406e..a477a18 100755 --- a/comicapi/comicarchive.py +++ b/comicapi/comicarchive.py @@ -28,10 +28,10 @@ import shutil from natsort import natsorted try: - from unrar import rarfile - from unrar import unrarlib - import unrar.constants - from unrar import constants + import rarfile + #from unrar import unrarlib + #import unrar.constants + #from unrar import constants rarsupport = True except ImportError: rarsupport = False @@ -39,7 +39,7 @@ import ctypes import io -if rarsupport: +'''if rarsupport: class OpenableRarFile(rarfile.RarFile): def open(self, member): #print "opening %s..." % member @@ -75,7 +75,7 @@ if rarsupport: self._close(handle) if not found: raise KeyError('There is no item named %r in the archive' % member) - return b''.join(buf) + return b''.join(buf)''' # if platform.system() == "Windows": @@ -372,17 +372,9 @@ if rarsupport: while tries < 7: try: tries = tries + 1 - #tmp_folder = tempfile.mkdtemp() - #tmp_file = os.path.join(tmp_folder, archive_file) - #rarc.extract(archive_file, tmp_folder) - data = rarc.open(archive_file) - #data = open(tmp_file).read() + data = rarc.read(archive_file) entries = [(rarc.getinfo(archive_file), data)] - #shutil.rmtree(tmp_folder, ignore_errors=True) - - #entries = rarc.read_files( archive_file ) - if entries[0][0].file_size != len(entries[0][1]): errMsg = u"readArchiveFile(): [file is not expected size: {0} vs {1}] {2}:{3} [attempt # {4}]".format( entries[0][0].file_size, len(entries[0][1]), self.path, @@ -502,7 +494,8 @@ if rarsupport: try: tries = tries + 1 #rarc = UnRAR2.RarFile( self.path ) - rarc = OpenableRarFile(self.path) + rarc = rarfile.RarFile(self.path) + #rarc = OpenableRarFile(self.path) except (OSError, IOError) as e: errMsg = u"getRARObj(): [{0}] {1} attempt#{2}".format( @@ -903,7 +896,7 @@ class ComicArchive: # k = os.path.join(os.path.split(k)[0], "z" + basename) return k.lower() - files = natsorted(files, key=keyfunc, signed=False) + files = natsorted(files, key=keyfunc) #, signed=False) # make a sub-list of image files self.page_list = [] diff --git a/setup.py b/setup.py index fad7a56..b29c707 100644 --- a/setup.py +++ b/setup.py @@ -1,15 +1,15 @@ from setuptools import setup setup( name = 'comicapi', - version = '2.0', + version = '2.1', description = 'Comic archive (cbr/cbz) and metadata utilities. Extracted from the comictagger project.', author = 'Iris W', packages = ['comicapi'], - install_requires = ['natsort>=3.5.2', 'pypdf2>=1.26'], + install_requires = ['natsort>=3.5.2'], extras_require = { - 'CBR': ['unrar==0.3'] + 'CBR': ['rarfile==2.7'] }, python_requires = '>=2.7.0', - url = 'https://github.com/wildthyme/comicapi', + url = 'https://github.com/OzzieIsaacs/comicapi', classifiers = ['License :: OSI Approved :: Apache Software License 2.0 (Apache-2.0)'] ) From 3e15b950b72724b1b8ca619c36580b5fbaba9784 Mon Sep 17 00:00:00 2001 From: Ozzieisaacs Date: Sat, 23 May 2020 16:09:10 +0200 Subject: [PATCH 17/22] Integrated cbt/tar file support Added logging capability Code cosmetics --- README.md | 2 +- comicapi/__init__.py | 2 + comicapi/comet.py | 26 +-- comicapi/comicarchive.py | 380 ++++++++++++++++++++++++------------ comicapi/comicbookinfo.py | 23 +-- comicapi/comicinfoxml.py | 14 +- comicapi/filenameparser.py | 19 +- comicapi/genericmetadata.py | 26 +-- comicapi/issuestring.py | 16 +- comicapi/utils.py | 54 ++--- setup.py | 4 +- 11 files changed, 327 insertions(+), 239 deletions(-) mode change 100755 => 100644 comicapi/comet.py mode change 100755 => 100644 comicapi/comicarchive.py mode change 100755 => 100644 comicapi/comicbookinfo.py mode change 100755 => 100644 comicapi/comicinfoxml.py mode change 100755 => 100644 comicapi/filenameparser.py mode change 100755 => 100644 comicapi/genericmetadata.py mode change 100755 => 100644 comicapi/issuestring.py mode change 100755 => 100644 comicapi/utils.py diff --git a/README.md b/README.md index e858949..c02309c 100644 --- a/README.md +++ b/README.md @@ -2,4 +2,4 @@ comicapi originates [here](https://github.com/davide-romanini/comicapi), was integrated into [ComicStreamer](https://github.com/davide-romanini/ComicStreamer), was modified in [this fork](https://github.com/kounch/ComicStreamer), and has now been extracted and packaged by yours truly (Iris W). # Installation -you can use pip to install this. cbr support is off by default—you'll need to do `pip install unrar` as well as having libunrar.so available. +you can use pip to install this. cbr support is off by default—you'll need to do `pip install rarfile` as well as having "unrar" available. diff --git a/comicapi/__init__.py b/comicapi/__init__.py index 0d9bd7c..94f9acf 100644 --- a/comicapi/__init__.py +++ b/comicapi/__init__.py @@ -1 +1,3 @@ __author__ = 'dromanin' + +__version__ = '2.1.1' diff --git a/comicapi/comet.py b/comicapi/comet.py old mode 100755 new mode 100644 index fc0a00e..8355370 --- a/comicapi/comet.py +++ b/comicapi/comet.py @@ -1,5 +1,5 @@ """ -A python class to encapsulate CoMet data +A python class to encapsulate CoMet data Copyright 2012-2014 Anthony Beville @@ -7,7 +7,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -16,9 +16,6 @@ See the License for the specific language governing permissions and limitations under the License. """ -from datetime import datetime -import zipfile -from pprint import pprint import xml.etree.ElementTree as ET from comicapi.genericmetadata import GenericMetadata import comicapi.utils @@ -64,7 +61,7 @@ class CoMet: def convertMetadataToXML(self, filename, metadata): - #shorthand for the metadata + # shorthand for the metadata md = metadata # build a tree structure @@ -74,7 +71,7 @@ class CoMet: root.attrib[ 'xsi:schemaLocation'] = "http://www.denvog.com http://www.denvog.com/comet/comet.xsd" - #helper func + # helper func def assign(comet_entry, md_entry): if md_entry is not None: ET.SubElement(root, comet_entry).text = u"{0}".format(md_entry) @@ -84,7 +81,7 @@ class CoMet: md.title = "" assign('title', md.title) assign('series', md.series) - assign('issue', md.issue) #must be int?? + assign('issue', md.issue) # must be int?? assign('volume', md.volume) assign('description', md.comments) assign('publisher', md.publisher) @@ -116,15 +113,6 @@ class CoMet: assign('coverImage', md.coverImage) - # need to specially process the credits, since they are structured differently than CIX - credit_writer_list = list() - credit_penciller_list = list() - credit_inker_list = list() - credit_colorist_list = list() - credit_letterer_list = list() - credit_cover_list = list() - credit_editor_list = list() - # loop thru credits, and build a list for each role that CoMet supports for credit in metadata.credits: @@ -169,7 +157,6 @@ class CoMet: if root.tag != 'comet': raise KeyError("Not a comet XML!") - #return None metadata = GenericMetadata() md = metadata @@ -234,7 +221,7 @@ class CoMet: return metadata - #verify that the string actually contains CoMet data in XML format + # verify that the string actually contains CoMet data in XML format def validateString(self, string): try: tree = ET.ElementTree(ET.fromstring(string)) @@ -249,7 +236,6 @@ class CoMet: def writeToExternalFile(self, filename, metadata): tree = self.convertMetadataToXML(self, metadata) - #ET.dump(tree) tree.write(filename, encoding='utf-8') def readFromExternalFile(self, filename): diff --git a/comicapi/comicarchive.py b/comicapi/comicarchive.py old mode 100755 new mode 100644 index a477a18..3370a85 --- a/comicapi/comicarchive.py +++ b/comicapi/comicarchive.py @@ -17,84 +17,36 @@ limitations under the License. """ import zipfile +import tarfile import os import struct import sys import tempfile import subprocess import platform -import locale -import shutil +import logging from natsort import natsorted try: import rarfile - #from unrar import unrarlib - #import unrar.constants - #from unrar import constants rarsupport = True except ImportError: rarsupport = False -import ctypes -import io - -'''if rarsupport: - class OpenableRarFile(rarfile.RarFile): - def open(self, member): - #print "opening %s..." % member - # based on https://github.com/matiasb/python-unrar/pull/4/files - if isinstance(member, rarfile.RarInfo): - member = member.filename - archive = unrarlib.RAROpenArchiveDataEx( - self.filename, mode=constants.RAR_OM_EXTRACT) - handle = self._open(archive) - found, buf = False, [] - - def _callback(msg, UserData, P1, P2): - if msg == constants.UCM_PROCESSDATA: - data = (ctypes.c_char * P2).from_address(P1).raw - buf.append(data) - return 1 - - c_callback = unrarlib.UNRARCALLBACK(_callback) - unrarlib.RARSetCallback(handle, c_callback, 1) - try: - rarinfo = self._read_header(handle) - while rarinfo is not None: - #print "checking rar archive %s against %s" % (rarinfo.filename, member) - if rarinfo.filename == member: - self._process_current(handle, constants.RAR_TEST) - found = True - else: - self._process_current(handle, constants.RAR_SKIP) - rarinfo = self._read_header(handle) - except unrarlib.UnrarException: - raise rarfile.BadRarFile("Bad RAR archive data.") - finally: - self._close(handle) - if not found: - raise KeyError('There is no item named %r in the archive' % member) - return b''.join(buf)''' - - -# if platform.system() == "Windows": -# import _subprocess import time from io import StringIO -from io import BytesIO try: from PIL import Image pil_available = True except ImportError: pil_available = False -sys.path.insert(0, os.path.abspath(".")) -#import UnRAR2 -#from UnRAR2.rar_exceptions import * +logger = logging.getLogger(__name__) +logger.setLevel(logging.INFO) + +sys.path.insert(0, os.path.abspath(".")) -#from settings import ComicTaggerSettings from comicapi.comicinfoxml import ComicInfoXml from comicapi.comicbookinfo import ComicBookInfo from comicapi.comet import CoMet @@ -124,7 +76,6 @@ class ZipArchiver: return self.writeZipComment(self.path, comment) def readArchiveFile(self, archive_file): - data = "" zf = zipfile.ZipFile(self.path, 'r') try: @@ -132,14 +83,14 @@ class ZipArchiver: except zipfile.BadZipfile as e: errMsg = u"bad zipfile [{0}]: {1} :: {2}".format( e, self.path, archive_file) - sys.stderr.buffer.write(bytes(errMsg, "UTF-8")) + logger.info(errMsg) zf.close() raise IOError except Exception as e: zf.close() errMsg = u"bad zipfile [{0}]: {1} :: {2}".format( e, self.path, archive_file) - sys.stderr.buffer.write(bytes(errMsg, "UTF-8")) + logger.info(errMsg) raise IOError finally: zf.close() @@ -160,7 +111,7 @@ class ZipArchiver: try: self.rebuildZipFile([archive_file]) - #now just add the archive file as a new one + # now just add the archive file as a new one zf = zipfile.ZipFile( self.path, mode='a', compression=zipfile.ZIP_DEFLATED) zf.writestr(archive_file, data) @@ -178,14 +129,14 @@ class ZipArchiver: except Exception as e: errMsg = u"Unable to get zipfile list [{0}]: {1}".format( e, self.path) - sys.stderr.buffer.write(bytes(errMsg, "UTF-8")) + logger.info(errMsg) return [] # zip helper func def rebuildZipFile(self, exclude_list): # this recompresses the zip archive, without the files in the exclude_list - #errMsg=u"Rebuilding zip {0} without {1}".format( self.path, exclude_list ) + # errMsg=u"Rebuilding zip {0} without {1}".format( self.path, exclude_list ) # generate temp file tmp_fd, tmp_name = tempfile.mkstemp(dir=os.path.dirname(self.path)) @@ -195,10 +146,10 @@ class ZipArchiver: zout = zipfile.ZipFile(tmp_name, 'w') for item in zin.infolist(): buffer = zin.read(item.filename) - if (item.filename not in exclude_list): + if item.filename not in exclude_list: zout.writestr(item, buffer) - #preserve the old comment + # preserve the old comment zout.comment = zin.comment zout.close() @@ -218,14 +169,13 @@ class ZipArchiver: see: http://en.wikipedia.org/wiki/Zip_(file_format)#Structure """ - #get file size statinfo = os.stat(filename) file_length = statinfo.st_size try: fo = open(filename, "r+b") - #the starting position, relative to EOF + # the starting position, relative to EOF pos = -4 found = False @@ -238,13 +188,13 @@ class ZipArchiver: value = fo.read(4) - #look for the end of central directory signature + # look for the end of central directory signature if bytearray(value) == bytearray([0x50, 0x4b, 0x05, 0x06]): found = True else: # not found, step back another byte pos = pos - 1 - #print pos,"{1} int: {0:x}".format(bytearray(value)[0], value) + # print pos,"{1} int: {0:x}".format(bytearray(value)[0], value) if found: @@ -282,22 +232,200 @@ class ZipArchiver: zout.writestr(fname, data) zout.close() - #preserve the old comment + # preserve the old comment comment = otherArchive.getArchiveComment() if comment is not None: if not self.writeZipComment(self.path, comment): return False except Exception as e: errMsg = u"Error while copying to {0}: {1}".format(self.path, e) - sys.stderr.buffer.write(bytes(errMsg, "UTF-8")) + logger.info(errMsg) return False else: return True -#------------------------------------------ +class TarArchiver: + def __init__(self, path): + self.path = path + + def getArchiveComment(self): + tf = tarfile.TarFile(self.path, 'r') + comment = tf.comment + tf.close() + return comment + + def setArchiveComment(self, comment): + return self.writeTarComment(self.path, comment) + + def readArchiveFile(self, archive_file): + tf = tarfile.TarFile(self.path, 'r') + + try: + data = tf.extractfile(archive_file).read() + except tarfile.TarError as e: + errMsg = u"bad tarfile [{0}]: {1} :: {2}".format( + e, self.path, archive_file) + logger.info(errMsg) + tf.close() + raise IOError + except Exception as e: + tf.close() + errMsg = u"bad tarfile [{0}]: {1} :: {2}".format( + e, self.path, archive_file) + logger.info(errMsg) + raise IOError + finally: + tf.close() + return data + + def removeArchiveFile(self, archive_file): + try: + self.rebuildTarFile([archive_file]) + except: + return False + else: + return True + + def writeArchiveFile(self, archive_file, data): + # At the moment, no other option but to rebuild the whole + # zip archive w/o the indicated file. Very sucky, but maybe + # another solution can be found + try: + self.rebuildTarFile([archive_file]) + + # now just add the archive file as a new one + tf = tarfile.Tarfile( + self.path, mode='a') + tf.writestr(archive_file, data) + tf.close() + return True + except: + return False + + def getArchiveFilenameList(self): + try: + tf = tarfile.TarFile(self.path, 'r') + namelist = tf.getnames() + tf.close() + return namelist + except Exception as e: + errMsg = u"Unable to get tarfile list [{0}]: {1}".format( + e, self.path) + logger.info(errMsg) + return [] + + # zip helper func + def rebuildTarFile(self, exclude_list): + + # this recompresses the zip archive, without the files in the exclude_list + # errMsg=u"Rebuilding zip {0} without {1}".format( self.path, exclude_list ) + + # generate temp file + tmp_fd, tmp_name = tempfile.mkstemp(dir=os.path.dirname(self.path)) + os.close(tmp_fd) + + tin = tarfile.TarFile(self.path, 'r') + tout = tarfile.TarFile(tmp_name, 'w') + for item in tin.infolist(): + buffer = tin.read(item.filename) + if (item.filename not in exclude_list): + tout.writestr(item, buffer) + + # preserve the old comment + tout.comment = tin.comment + + tout.close() + tin.close() + + # replace with the new file + os.remove(self.path) + os.rename(tmp_name, self.path) + + def writeTarComment(self, filename, comment): + """ + This is a custom function for writing a comment to a tar file, + since the built-in one doesn't seem to work on Windows and Mac OS/X + """ + + statinfo = os.stat(filename) + file_length = statinfo.st_size + + try: + fo = open(filename, "r+b") + + # the starting position, relative to EOF + pos = -4 + + found = False + value = bytearray() + + # walk backwards to find the "End of Central Directory" record + while (not found) and (-pos != file_length): + # seek, relative to EOF + fo.seek(pos, 2) + + value = fo.read(4) + + # look for the end of central directory signature + if bytearray(value) == bytearray([0x50, 0x4b, 0x05, 0x06]): + found = True + else: + # not found, step back another byte + pos = pos - 1 + + if found: + + # now skip forward 20 bytes to the comment length word + pos += 20 + fo.seek(pos, 2) + + # Pack the length of the comment string + format = "H" # one 2-byte integer + comment_length = struct.pack( + format, len(comment)) # pack integer in a binary string + + # write out the length + fo.write(comment_length) + fo.seek(pos + 2, 2) + + # write out the comment itself + fo.write(comment) + fo.truncate() + fo.close() + else: + raise Exception('Failed to write comment to tar file!') + except: + return False + else: + return True + + def copyFromArchive(self, otherArchive): + # Replace the current zip with one copied from another archive + try: + tout = tarfile.TarFile(self.path, 'w') + for fname in otherArchive.getArchiveFilenameList(): + data = otherArchive.readArchiveFile(fname) + if data is not None: + tout.writestr(fname, data) + tout.close() + + # preserve the old comment + comment = otherArchive.getArchiveComment() + if comment is not None: + if not self.writeTarComment(self.path, comment): + return False + except Exception as e: + errMsg = u"Error while copying to {0}: {1}".format(self.path, e) + logger.info(errMsg) + return False + else: + return True + +# ------------------------------------------ # RAR implementation + if rarsupport: class RarArchiver: @@ -319,7 +447,7 @@ if rarsupport: self.startupinfo = None def __del__(self): - #RarArchiver.devnull.close() + # RarArchiver.devnull.close() pass def getArchiveComment(self): @@ -363,8 +491,6 @@ if rarsupport: # Make sure to escape brackets, since some funky stuff is going on # underneath with "fnmatch" - #archive_file = archive_file.replace("[", '[[]') - entries = [] rarc = self.getRARObj() @@ -376,30 +502,30 @@ if rarsupport: entries = [(rarc.getinfo(archive_file), data)] if entries[0][0].file_size != len(entries[0][1]): - errMsg = u"readArchiveFile(): [file is not expected size: {0} vs {1}] {2}:{3} [attempt # {4}]".format( - entries[0][0].file_size, len(entries[0][1]), self.path, - archive_file, tries) - sys.stderr.buffer.write(bytes(errMsg, "UTF-8")) + errMsg = u"readArchiveFile(): " \ + u"[file is not expected size: {0} vs {1}] {2}:{3} [attempt # {4}]\n".format( + entries[0][0].file_size, len(entries[0][1]), self.path, archive_file, tries) + logger.info(errMsg) continue except (OSError, IOError) as e: errMsg = u"readArchiveFile(): [{0}] {1}:{2} attempt#{3}".format( str(e), self.path, archive_file, tries) - sys.stderr.buffer.write(bytes(errMsg, "UTF-8")) + logger.info(errMsg) time.sleep(1) except Exception as e: errMsg = u"Unexpected exception in readArchiveFile(): [{0}] for {1}:{2} attempt#{3}".format( str(e), self.path, archive_file, tries) - sys.stderr.buffer.write(bytes(errMsg, "UTF-8")) + logger.info(errMsg) break else: - #Success" - #entries is a list of of tuples: ( rarinfo, filedata) + # Success" + # entries is a list of of tuples: ( rarinfo, filedata) if tries > 1: errMsg = u"Attempted read_files() {0} times".format(tries) - sys.stderr.buffer.write(bytes(errMsg, "UTF-8")) - if (len(entries) == 1): + logger.info(errMsg) + if len(entries) == 1: return entries[0][1] else: raise IOError @@ -463,14 +589,14 @@ if rarsupport: def getArchiveFilenameList(self): rarc = self.getRARObj() - #namelist = [ item.filename for item in rarc.infolist() ] - #return namelist + # namelist = [ item.filename for item in rarc.infolist() ] + # return namelist tries = 0 while tries < 7: try: tries = tries + 1 - #namelist = [ item.filename for item in rarc.infolist() ] + # namelist = [ item.filename for item in rarc.infolist() ] namelist = [] for item in rarc.infolist(): if item.file_size != 0: @@ -479,11 +605,11 @@ if rarsupport: except (OSError, IOError) as e: errMsg = u"getArchiveFilenameList(): [{0}] {1} attempt#{2}".format( str(e), self.path, tries) - sys.stderr.buffer.write(bytes(errMsg, "UTF-8")) + logger.info(errMsg) time.sleep(1) else: - #Success" + # Success" return namelist raise e @@ -493,24 +619,22 @@ if rarsupport: while tries < 7: try: tries = tries + 1 - #rarc = UnRAR2.RarFile( self.path ) rarc = rarfile.RarFile(self.path) - #rarc = OpenableRarFile(self.path) except (OSError, IOError) as e: errMsg = u"getRARObj(): [{0}] {1} attempt#{2}".format( str(e), self.path, tries) - sys.stderr.buffer.write(bytes(errMsg, "UTF-8")) + logger.info(errMsg) time.sleep(1) else: - #Success" + # Success" return rarc raise e -#------------------------------------------ +# ------------------------------------------ # Folder implementation class FolderArchiver: def __init__(self, path): @@ -531,7 +655,7 @@ class FolderArchiver: with open(fname, 'rb') as f: data = f.read() f.close() - except IOError as e: + except IOError: pass return data @@ -573,7 +697,7 @@ class FolderArchiver: return itemlist -#------------------------------------------ +# ------------------------------------------ # Unknown implementation class UnknownArchiver: def __init__(self, path): @@ -628,13 +752,13 @@ class PdfArchiver: return out -#------------------------------------------------------------------ +# ------------------------------------------------------------------ class ComicArchive: logo_data = None class ArchiveType: - Zip, Rar, Folder, Pdf, Unknown = range(5) + Zip, Rar, Tar, Folder, Pdf, Unknown = range(6) def __init__(self, path, rar_exe_path=None, default_image_path=None): self.path = path @@ -665,6 +789,10 @@ class ComicArchive: self.archive_type = self.ArchiveType.Zip self.archiver = ZipArchiver(self.path) + if self.tarTest(): + self.archive_type = self.ArchiveType.Tar + self.archiver = TarArchiver(self.path) + elif self.rarTest(): self.archive_type = self.ArchiveType.Rar self.archiver = RarArchiver( @@ -674,7 +802,6 @@ class ComicArchive: self.archiver = PdfArchiver(self.path) if ComicArchive.logo_data is None and self.default_image_path: - #fname = ComicTaggerSettings.getGraphic('nocover.png') fname = self.default_image_path with open(fname, 'rb') as fd: ComicArchive.logo_data = fd.read() @@ -702,6 +829,9 @@ class ComicArchive: def zipTest(self): return zipfile.is_zipfile(self.path) + def tarTest(self): + return tarfile.is_tarfile(self.path) + def rarTest(self): try: rarc = rarfile.RarFile(self.path) @@ -713,6 +843,9 @@ class ComicArchive: def isZip(self): return self.archive_type == self.ArchiveType.Zip + def isTar(self): + return self.archive_type == self.ArchiveType.Tar + def isRar(self): return self.archive_type == self.ArchiveType.Rar @@ -748,11 +881,8 @@ class ComicArchive: def seemsToBeAComicArchive(self): - # Do we even care about extensions?? - ext = os.path.splitext(self.path)[1].lower() - - if ((self.isZip() or self.isRar() or self.isPdf() - ) #or self.isFolder() ) + if ((self.isZip() or self.isRar() or self.isPdf() or self.isTar() + ) and (self.getNumberOfPages() > 0)): return True else: @@ -811,8 +941,8 @@ class ComicArchive: try: image_data = self.archiver.readArchiveFile(filename) except IOError: - errMsg = u"Error reading in page. Substituting logo page." - sys.stderr.buffer.write(bytes(errMsg, "UTF-8")) + errMsg = u"Error reading in page. Substituting logo page." + logger.info(errMsg) image_data = ComicArchive.logo_data return image_data @@ -834,11 +964,11 @@ class ComicArchive: scanner_page_index = None - #make a guess at the scanner page + # make a guess at the scanner page name_list = self.getPageNameList() count = self.getNumberOfPages() - #too few pages to really know + # too few pages to really know if count < 5: return None @@ -870,7 +1000,7 @@ class ComicArchive: prefix = os.path.commonprefix(common_length_list) if mode_length <= 7 and prefix == "": - #probably all numbers + # probably all numbers if len(final_name) > mode_length: scanner_page_index = count - 1 @@ -890,13 +1020,13 @@ class ComicArchive: if sort_list: def keyfunc(k): - #hack to account for some weird scanner ID pages - #basename=os.path.split(k)[1] - #if basename < '0': + # hack to account for some weird scanner ID pages + # basename=os.path.split(k)[1] + # if basename < '0': # k = os.path.join(os.path.split(k)[0], "z" + basename) return k.lower() - files = natsorted(files, key=keyfunc) #, signed=False) + files = natsorted(files, key=keyfunc) #, signed=False) # make a sub-list of image files self.page_list = [] @@ -927,15 +1057,15 @@ class ComicArchive: return self.cbi_md def readRawCBI(self): - if (not self.hasCBI()): + if not self.hasCBI(): return None - - return self.archiver.getArchiveComment() + else: + return self.archiver.getArchiveComment() def hasCBI(self): if self.has_cbi is None: - #if ( not ( self.isZip() or self.isRar()) or not self.seemsToBeAComicArchive() ): + # if ( not ( self.isZip() or self.isRar()) or not self.seemsToBeAComicArchive() ): if not self.seemsToBeAComicArchive(): self.has_cbi = False else: @@ -975,7 +1105,7 @@ class ComicArchive: else: self.cix_md = ComicInfoXml().metadataFromString(raw_cix) - #validate the existing page list (make sure count is correct) + # validate the existing page list (make sure count is correct) if len(self.cix_md.pages) != 0: if len(self.cix_md.pages) != self.getNumberOfPages(): # pages array doesn't match the actual number of images we're seeing @@ -1044,7 +1174,7 @@ class ComicArchive: self.comet_md = CoMet().metadataFromString(raw_comet) self.comet_md.setDefaultPageList(self.getNumberOfPages()) - #use the coverImage value from the comet_data to mark the cover in this struct + # use the coverImage value from the comet_data to mark the cover in this struct # walk through list of images in file, and find the matching one for md.coverImage # need to remove the existing one in the default if self.comet_md.coverImage is not None: @@ -1063,14 +1193,14 @@ class ComicArchive: def readRawCoMet(self): if not self.hasCoMet(): errMsg = u"{} doesn't have CoMet data!".format(self.path) - sys.stderr.buffer.write(bytes(errMsg, "UTF-8")) + logger.info(errMsg) return None try: raw_comet = self.archiver.readArchiveFile(self.comet_filename) except IOError: errMsg = u"Error reading in raw CoMet!" - sys.stderr.buffer.write(bytes(errMsg, "UTF-8")) + logger.info(errMsg) raw_comet = "" return raw_comet @@ -1114,7 +1244,7 @@ class ComicArchive: if not self.seemsToBeAComicArchive(): return self.has_comet - #look at all xml files in root, and search for CoMet data, get first + # look at all xml files in root, and search for CoMet data, get first for n in self.archiver.getArchiveFilenameList(): if (os.path.dirname(n) == "" and os.path.splitext(n)[1].lower() == '.xml'): @@ -1124,7 +1254,7 @@ class ComicArchive: except: data = "" errMsg = u"Error reading in Comet XML for validation!" - sys.stderr.buffer.write(bytes(errMsg, "UTF-8")) + logger.info(errMsg) if CoMet().validateString(data): # since we found it, save it! self.comet_filename = n @@ -1182,11 +1312,3 @@ class ComicArchive: metadata.isEmpty = False return metadata - - def exportAsZip(self, zipfilename): - if self.archive_type == self.ArchiveType.Zip: - # nothing to do, we're already a zip - return True - - zip_archiver = ZipArchiver(zipfilename) - return zip_archiver.copyFromArchive(self.archiver) diff --git a/comicapi/comicbookinfo.py b/comicapi/comicbookinfo.py old mode 100755 new mode 100644 index 2076b9a..008d7ef --- a/comicapi/comicbookinfo.py +++ b/comicapi/comicbookinfo.py @@ -1,5 +1,5 @@ """ -A python class to encapsulate the ComicBookInfo data +A python class to encapsulate the ComicBookInfo data Copyright 2012-2014 Anthony Beville @@ -7,7 +7,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -18,13 +18,10 @@ limitations under the License. import json from datetime import datetime -import zipfile from comicapi.genericmetadata import GenericMetadata import comicapi.utils -#import ctversion - class ComicBookInfo: def metadataFromString(self, string): @@ -35,7 +32,7 @@ class ComicBookInfo: cbi = cbi_container['ComicBookInfo/1.0'] - #helper func + # helper func # If item is not in CBI, return None def xlate(cbi_entry): if cbi_entry in cbi: @@ -66,7 +63,7 @@ class ComicBookInfo: if metadata.tags is None: metadata.tags = [] - #need to massage the language string to be ISO + # need to massage the language string to be ISO if metadata.language is not None: # reverse look-up pattern = metadata.language @@ -86,7 +83,7 @@ class ComicBookInfo: cbi_container = self.createJSONDictionary(metadata) return json.dumps(cbi_container) - #verify that the string actually contains CBI data in JSON format + # verify that the string actually contains CBI data in JSON format def validateString(self, string): try: @@ -94,24 +91,24 @@ class ComicBookInfo: except: return False - return ('ComicBookInfo/1.0' in cbi_container) + return 'ComicBookInfo/1.0' in cbi_container def createJSONDictionary(self, metadata): # Create the dictionary that we will convert to JSON text cbi = dict() cbi_container = { - 'appID': 'ComicTagger/' + '1.0.0', #ctversion.version, + 'appID': 'ComicTagger/' + '1.0.0', # ctversion.version, 'lastModified': str(datetime.now()), 'ComicBookInfo/1.0': cbi } - #helper func + # helper func def assign(cbi_entry, md_entry): if md_entry is not None: cbi[cbi_entry] = md_entry - #helper func + # helper func def toInt(s): i = None if type(s) in [str, int]: @@ -147,4 +144,4 @@ class ComicBookInfo: f = open(filename, 'w') f.write(json.dumps(cbi_container, indent=4)) - f.close + f.close() diff --git a/comicapi/comicinfoxml.py b/comicapi/comicinfoxml.py old mode 100755 new mode 100644 index 3d6f4ce..4a2503b --- a/comicapi/comicinfoxml.py +++ b/comicapi/comicinfoxml.py @@ -1,5 +1,5 @@ """ -A python class to encapsulate ComicRack's ComicInfo.xml data +A python class to encapsulate ComicRack's ComicInfo.xml data Copyright 2012-2014 Anthony Beville @@ -7,7 +7,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -16,9 +16,6 @@ See the License for the specific language governing permissions and limitations under the License. """ -from datetime import datetime -import zipfile -from pprint import pprint import xml.etree.ElementTree as ET from comicapi.genericmetadata import GenericMetadata import comicapi.utils @@ -75,7 +72,7 @@ class ComicInfoXml: def convertMetadataToXML(self, filename, metadata): - #shorthand for the metadata + # shorthand for the metadata md = metadata # build a tree structure @@ -83,7 +80,7 @@ class ComicInfoXml: root.attrib['xmlns:xsi'] = "http://www.w3.org/2001/XMLSchema-instance" root.attrib['xmlns:xsd'] = "http://www.w3.org/2001/XMLSchema" - #helper func + # helper func def assign(cix_entry, md_entry): if md_entry is not None: ET.SubElement(root, cix_entry).text = u"{0}".format(md_entry) @@ -267,7 +264,7 @@ class ComicInfoXml: if pages_node is not None: for page in pages_node: metadata.pages.append(page.attrib) - #print page.attrib + # print page.attrib metadata.isEmpty = False @@ -276,7 +273,6 @@ class ComicInfoXml: def writeToExternalFile(self, filename, metadata): tree = self.convertMetadataToXML(self, metadata) - #ET.dump(tree) tree.write(filename, encoding='utf-8') def readFromExternalFile(self, filename): diff --git a/comicapi/filenameparser.py b/comicapi/filenameparser.py old mode 100755 new mode 100644 index d672c6b..f36fea6 --- a/comicapi/filenameparser.py +++ b/comicapi/filenameparser.py @@ -1,5 +1,5 @@ """ -Functions for parsing comic info from filename +Functions for parsing comic info from filename This should probably be re-written, but, well, it mostly works! @@ -9,7 +9,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -37,7 +37,7 @@ class FileNameParser: placeholders = ['[_]', ' +'] for ph in placeholders: string = re.sub(ph, self.repl, string) - return string #.strip() + return string def getIssueCount(self, filename, issue_end): @@ -57,7 +57,6 @@ class FileNameParser: match = re.search('(?<=\(of\s)\d+(?=\))', tmpstr, re.IGNORECASE) if match: count = match.group() - found = True count = count.lstrip("0") @@ -94,8 +93,6 @@ class FileNameParser: # remove any "of NN" phrase with spaces (problem: this could break on some titles) filename = re.sub("of [\d]+", self.repl, filename) - #print u"[{0}]".format(filename) - # we should now have a cleaned up filename version with all the words in # the same positions as original filename @@ -108,7 +105,7 @@ class FileNameParser: if len(word_list) > 1: word_list = word_list[1:] else: - #only one word?? just bail. + # only one word?? just bail. return issue, start, end # Now try to search for the likely issue number word in the list @@ -164,7 +161,7 @@ class FileNameParser: series = tmpstr volume = "" - #save the last word + # save the last word try: last_word = series.split()[-1] except: @@ -182,7 +179,7 @@ class FileNameParser: # if a volume wasn't found, see if the last word is a year in parentheses # since that's a common way to designate the volume if volume == "": - #match either (YEAR), (YEAR-), or (YEAR-YEAR2) + # match either (YEAR), (YEAR-), or (YEAR-YEAR2) match = re.search("(\()(\d{4})(-(\d{4}|)|)(\))", last_word) if match: volume = match.group(2) @@ -218,7 +215,7 @@ class FileNameParser: def getRemainder(self, filename, year, count, issue_end): - #make a guess at where the the non-interesting stuff begins + # make a guess at where the the non-interesting stuff begins remainder = "" if "--" in filename: @@ -246,7 +243,7 @@ class FileNameParser: # remove the extension filename = os.path.splitext(filename)[0] - #url decode, just in case + # url decode, just in case filename = unquote(filename) # sometimes archives get messed up names from too many decodings diff --git a/comicapi/genericmetadata.py b/comicapi/genericmetadata.py old mode 100755 new mode 100644 index de1b3fa..b2c9bd9 --- a/comicapi/genericmetadata.py +++ b/comicapi/genericmetadata.py @@ -1,17 +1,17 @@ """ A python class for internal metadata storage - + The goal of this class is to handle ALL the data that might come from various - tagging schemes and databases, such as ComicVine or GCD. This makes conversion + tagging schemes and databases, such as ComicVine or GCD. This makes conversion possible, however lossy it might be - + Copyright 2012-2014 Anthony Beville Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -38,18 +38,6 @@ class PageType: Deleted = "Deleted" -""" -class PageInfo: - Image = 0 - Type = PageType.Story - DoublePage = False - ImageSize = 0 - Key = "" - ImageWidth = 0 - ImageHeight = 0 -""" - - class GenericMetadata: def __init__(self): @@ -174,8 +162,8 @@ class GenericMetadata: def overlayCredits(self, new_credits): for c in new_credits: - if 'primary' in c: # if c.has_key('primary') and c['primary']: + if 'primary' in c: primary = True else: primary = False @@ -295,8 +283,8 @@ class GenericMetadata: for c in self.credits: primary = "" - if 'primary' in c: # if c.has_key('primary') and c['primary']: + if 'primary' in c: primary = " [P]" add_string("credit", c['role'] + ": " + c['person'] + primary) @@ -306,7 +294,7 @@ class GenericMetadata: flen = max(flen, len(i[0])) flen += 1 - #format the data nicely + # format the data nicely outstr = "" fmt_str = u"{0: <" + str(flen) + "} {1}\n" for i in vals: diff --git a/comicapi/issuestring.py b/comicapi/issuestring.py old mode 100755 new mode 100644 index b50454b..d10eb46 --- a/comicapi/issuestring.py +++ b/comicapi/issuestring.py @@ -17,7 +17,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -26,10 +26,6 @@ See the License for the specific language governing permissions and limitations under the License. """ -import comicapi.utils -import math -import re - class IssueString: def __init__(self, text): @@ -49,7 +45,7 @@ class IssueString: if len(text) == 0: return - #skip the minus sign if it's first + # skip the minus sign if it's first if text[0] == '-': start = 1 else: @@ -88,10 +84,8 @@ class IssueString: else: self.suffix = text - #print "num: {0} suf: {1}".format(self.num, self.suffix) - def asString(self, pad=0): - #return the float, left side zero-padded, with suffix attached + # return the float, left side zero-padded, with suffix attached if self.num is None: return self.suffix @@ -119,7 +113,7 @@ class IssueString: return num_s def asFloat(self): - #return the float, with no suffix + # return the float, with no suffix if self.suffix == u"½": if self.num is not None: return self.num + .5 @@ -128,7 +122,7 @@ class IssueString: return self.num def asInt(self): - #return the int version of the float + # return the int version of the float if self.num is None: return None return int(self.num) diff --git a/comicapi/utils.py b/comicapi/utils.py old mode 100755 new mode 100644 index a02932d..56568b4 --- a/comicapi/utils.py +++ b/comicapi/utils.py @@ -9,7 +9,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -28,13 +28,15 @@ import codecs class UtilsVars: already_fixed_encoding = False + def get_actual_preferred_encoding(): preferred_encoding = locale.getpreferredencoding() if platform.system() == "Darwin": preferred_encoding = "utf-8" return preferred_encoding -def fix_output_encoding( ): + +def fix_output_encoding(): if not UtilsVars.already_fixed_encoding: # this reads the environment and inits the right locale locale.setlocale(locale.LC_ALL, "") @@ -45,37 +47,39 @@ def fix_output_encoding( ): sys.stderr = codecs.getwriter(preferred_encoding)(sys.stderr) UtilsVars.already_fixed_encoding = True -def get_recursive_filelist( pathlist ): + +def get_recursive_filelist(pathlist): + """ + Get a recursive list of of all files under all path items in the list """ - Get a recursive list of of all files under all path items in the list - """ filename_encoding = sys.getfilesystemencoding() filelist = [] for p in pathlist: # if path is a folder, walk it recursivly, and all files underneath if type(p) == str: - #make sure string is unicode - p = p.decode(filename_encoding) #, 'replace') + # make sure string is unicode + p = p.decode(filename_encoding) elif type(p) != str: - #it's probably a QString + # it's probably a QString p = str(p) - if os.path.isdir( p ): - for root,dirs,files in os.walk( p ): + if os.path.isdir(p): + for root, dirs, files in os.walk(p): for f in files: if type(f) == str: - #make sure string is unicode + # make sure string is unicode f = f.decode(filename_encoding, 'replace') elif type(f) != str: - #it's probably a QString + # it's probably a QString f = str(f) - filelist.append(os.path.join(root,f)) + filelist.append(os.path.join(root, f)) else: filelist.append(p) return filelist -def listToString( l ): + +def listToString(l): string = "" if l is not None: for item in l: @@ -84,17 +88,19 @@ def listToString( l ): string += item return string -def addtopath( dirname ): + +def addtopath(dirname): if dirname is not None and dirname != "": # verify that path doesn't already contain the given dirname tmpdirname = re.escape(dirname) - pattern = r"{sep}{dir}$|^{dir}{sep}|{sep}{dir}{sep}|^{dir}$".format( dir=tmpdirname, sep=os.pathsep) + pattern = r"{sep}{dir}$|^{dir}{sep}|{sep}{dir}{sep}|^{dir}$".format(dir=tmpdirname, sep=os.pathsep) match = re.search(pattern, os.environ['PATH']) if not match: os.environ['PATH'] = dirname + os.pathsep + os.environ['PATH'] + # returns executable path, if it exists def which(program): @@ -113,9 +119,10 @@ def which(program): return None -def removearticles( text ): + +def removearticles(text): text = text.lower() - articles = ['and', 'the', 'a', '&', 'issue' ] + articles = ['and', 'the', 'a', '&', 'issue'] newText = '' for word in text.split(' '): if word not in articles: @@ -131,16 +138,15 @@ def removearticles( text ): # since the CV api changed, searches for series names with periods # now explicity require the period to be in the search key, # so the line below is removed (for now) - #newText = newText.replace(".", "") return newText def unique_file(file_name): counter = 1 - file_name_parts = os.path.splitext(file_name) # returns ('/path/file', '.ext') + file_name_parts = os.path.splitext(file_name) # returns ('/path/file', '.ext') while 1: - if not os.path.lexists( file_name): + if not os.path.lexists(file_name): return file_name file_name = file_name_parts[0] + ' (' + str(counter) + ')' + file_name_parts[1] counter += 1 @@ -573,12 +579,12 @@ countries = [ ] - def getLanguageDict(): return lang_dict -def getLanguageFromISO( iso ): + +def getLanguageFromISO(iso): if iso == None: return None else: - return lang_dict[ iso ] + return lang_dict[iso] diff --git a/setup.py b/setup.py index b29c707..db0cf1f 100644 --- a/setup.py +++ b/setup.py @@ -1,8 +1,8 @@ from setuptools import setup setup( name = 'comicapi', - version = '2.1', - description = 'Comic archive (cbr/cbz) and metadata utilities. Extracted from the comictagger project.', + version = '2.1.1', + description = 'Comic archive (cbr/cbz/cbt) and metadata utilities. Extracted from the comictagger project.', author = 'Iris W', packages = ['comicapi'], install_requires = ['natsort>=3.5.2'], From b323fab55e7daba97f90bf59a4bc8de9d9c0a86b Mon Sep 17 00:00:00 2001 From: Ozzie Isaacs Date: Thu, 27 Aug 2020 21:02:16 +0200 Subject: [PATCH 18/22] UNRAR_TOOL variable is set on load archive --- comicapi/comicarchive.py | 1 + 1 file changed, 1 insertion(+) diff --git a/comicapi/comicarchive.py b/comicapi/comicarchive.py index 3370a85..e63b559 100644 --- a/comicapi/comicarchive.py +++ b/comicapi/comicarchive.py @@ -619,6 +619,7 @@ if rarsupport: while tries < 7: try: tries = tries + 1 + rarfile.UNRAR_TOOL = self.rar_exe_path rarc = rarfile.RarFile(self.path) except (OSError, IOError) as e: From 7aa91ea95ea7a919df6c6cac817865434a31c228 Mon Sep 17 00:00:00 2001 From: Ozzieisaacs Date: Mon, 30 Nov 2020 18:34:09 +0100 Subject: [PATCH 19/22] Fix for rarfile import on python3.5 --- comicapi/__init__.py | 2 +- comicapi/comicarchive.py | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/comicapi/__init__.py b/comicapi/__init__.py index 94f9acf..4f014bc 100644 --- a/comicapi/__init__.py +++ b/comicapi/__init__.py @@ -1,3 +1,3 @@ __author__ = 'dromanin' -__version__ = '2.1.1' +__version__ = '2.1.2' diff --git a/comicapi/comicarchive.py b/comicapi/comicarchive.py index e63b559..3bbd9a1 100644 --- a/comicapi/comicarchive.py +++ b/comicapi/comicarchive.py @@ -30,7 +30,7 @@ from natsort import natsorted try: import rarfile rarsupport = True -except ImportError: +except (ImportError, SyntaxError): rarsupport = False import time diff --git a/setup.py b/setup.py index db0cf1f..8c84aad 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ from setuptools import setup setup( name = 'comicapi', - version = '2.1.1', + version = '2.1.2', description = 'Comic archive (cbr/cbz/cbt) and metadata utilities. Extracted from the comictagger project.', author = 'Iris W', packages = ['comicapi'], From 6798cd2a83aa247417724ac17e866a4d2cf91d1d Mon Sep 17 00:00:00 2001 From: Ozzie Isaacs Date: Sun, 17 Jan 2021 10:30:00 +0100 Subject: [PATCH 20/22] Update version and classifier information --- .gitignore | 3 +++ comicapi/__init__.py | 2 +- setup.py | 9 +++++++-- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 5198972..b40db82 100644 --- a/.gitignore +++ b/.gitignore @@ -122,3 +122,6 @@ dmypy.json # Pyre type checker .pyre/ + +#pycharm +/.idea diff --git a/comicapi/__init__.py b/comicapi/__init__.py index 4f014bc..819eabc 100644 --- a/comicapi/__init__.py +++ b/comicapi/__init__.py @@ -1,3 +1,3 @@ __author__ = 'dromanin' -__version__ = '2.1.2' +__version__ = '2.1.3' diff --git a/setup.py b/setup.py index 8c84aad..0f55e9f 100644 --- a/setup.py +++ b/setup.py @@ -1,9 +1,10 @@ from setuptools import setup setup( name = 'comicapi', - version = '2.1.2', + version = '2.1.3', description = 'Comic archive (cbr/cbz/cbt) and metadata utilities. Extracted from the comictagger project.', author = 'Iris W', + maintainer = "@OzzieIsaacs", packages = ['comicapi'], install_requires = ['natsort>=3.5.2'], extras_require = { @@ -11,5 +12,9 @@ setup( }, python_requires = '>=2.7.0', url = 'https://github.com/OzzieIsaacs/comicapi', - classifiers = ['License :: OSI Approved :: Apache Software License 2.0 (Apache-2.0)'] + classifiers = [ + "Programming Language :: Python :: 3", + 'License :: OSI Approved :: Apache Software License', + "Operating System :: OS Independent", + ] ) From 59e7830f62c5f6e42aea16d171d336814dca826f Mon Sep 17 00:00:00 2001 From: Ozzie Isaacs Date: Thu, 18 Mar 2021 18:53:18 +0100 Subject: [PATCH 21/22] Update to 2.2.0 Removed pdf support -> PyPDF2 is not necessary any more --- comicapi/__init__.py | 2 +- comicapi/comicarchive.py | 34 ---------------------------------- setup.py | 2 +- 3 files changed, 2 insertions(+), 36 deletions(-) diff --git a/comicapi/__init__.py b/comicapi/__init__.py index 819eabc..823d73b 100644 --- a/comicapi/__init__.py +++ b/comicapi/__init__.py @@ -1,3 +1,3 @@ __author__ = 'dromanin' -__version__ = '2.1.3' +__version__ = '2.2.0' diff --git a/comicapi/comicarchive.py b/comicapi/comicarchive.py index 3bbd9a1..63110ad 100644 --- a/comicapi/comicarchive.py +++ b/comicapi/comicarchive.py @@ -52,7 +52,6 @@ from comicapi.comicbookinfo import ComicBookInfo from comicapi.comet import CoMet from comicapi.genericmetadata import GenericMetadata, PageType from comicapi.filenameparser import FileNameParser -from PyPDF2 import PdfFileReader class MetaDataStyle: @@ -723,36 +722,6 @@ class UnknownArchiver: return [] -class PdfArchiver: - def __init__(self, path): - self.path = path - - def getArchiveComment(self): - return "" - - def setArchiveComment(self, comment): - return False - - def readArchiveFile(self, page_num): - return subprocess.check_output([ - 'mudraw', '-o', '-', self.path, - str(int(os.path.basename(page_num)[:-4])) - ]) - - def writeArchiveFile(self, archive_file, data): - return False - - def removeArchiveFile(self, archive_file): - return False - - def getArchiveFilenameList(self): - out = [] - pdf = PdfFileReader(open(self.path, 'rb')) - for page in range(1, pdf.getNumPages() + 1): - out.append("/%04d.jpg" % (page)) - return out - - # ------------------------------------------------------------------ class ComicArchive: @@ -798,9 +767,6 @@ class ComicArchive: self.archive_type = self.ArchiveType.Rar self.archiver = RarArchiver( self.path, rar_exe_path=self.rar_exe_path) - elif os.path.basename(self.path)[-3:] == 'pdf': - self.archive_type = self.ArchiveType.Pdf - self.archiver = PdfArchiver(self.path) if ComicArchive.logo_data is None and self.default_image_path: fname = self.default_image_path diff --git a/setup.py b/setup.py index 0f55e9f..a12f099 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ from setuptools import setup setup( name = 'comicapi', - version = '2.1.3', + version = '2.2.0', description = 'Comic archive (cbr/cbz/cbt) and metadata utilities. Extracted from the comictagger project.', author = 'Iris W', maintainer = "@OzzieIsaacs", From 7267dac0ae093e442618570e38a0623ad3bc76d7 Mon Sep 17 00:00:00 2001 From: Ozzie Isaacs Date: Sat, 22 Jan 2022 11:26:30 +0100 Subject: [PATCH 22/22] Added support for bmp files version update --- comicapi/comicarchive.py | 2 +- setup.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/comicapi/comicarchive.py b/comicapi/comicarchive.py index 63110ad..313f600 100644 --- a/comicapi/comicarchive.py +++ b/comicapi/comicarchive.py @@ -999,7 +999,7 @@ class ComicArchive: self.page_list = [] for name in files: if (name[-4:].lower() in [ - ".jpg", "jpeg", ".png", ".gif", "webp" + ".jpg", "jpeg", ".png", ".gif", "webp", ".bmp" ] and os.path.basename(name)[0] != "."): self.page_list.append(name) diff --git a/setup.py b/setup.py index a12f099..7eb4b0a 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ from setuptools import setup setup( name = 'comicapi', - version = '2.2.0', + version = '2.2.1', description = 'Comic archive (cbr/cbz/cbt) and metadata utilities. Extracted from the comictagger project.', author = 'Iris W', maintainer = "@OzzieIsaacs", @@ -10,7 +10,7 @@ setup( extras_require = { 'CBR': ['rarfile==2.7'] }, - python_requires = '>=2.7.0', + python_requires = '>=3.0.0', url = 'https://github.com/OzzieIsaacs/comicapi', classifiers = [ "Programming Language :: Python :: 3",