Commands.py 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  1. from Globals import Globals
  2. from Kobo import Kobo, KoboException
  3. import colorama
  4. import os
  5. class Commands:
  6. # It wasn't possible to format the main help message to my liking, so using a custom one.
  7. # This was the most annoying:
  8. #
  9. # commands:
  10. # command <-- absolutely unneeded text
  11. # get List unread books
  12. # list Get book
  13. #
  14. # See https://stackoverflow.com/questions/13423540/ and https://stackoverflow.com/questions/11070268/
  15. @staticmethod
  16. def ShowUsage():
  17. usage = \
  18. """Kobo book downloader and DRM remover
  19. Usage:
  20. kobo-book-downloader [--help] command ...
  21. Commands:
  22. get Download book
  23. info Show the location of the configuration file
  24. list List your books
  25. Optional arguments:
  26. -h, --help Show this help message and exit
  27. Examples:
  28. kobo-book-downloader get /dir/book.epub 01234567-89ab-cdef-0123-456789abcdef Download book
  29. kobo-book-downloader get /dir/ 01234567-89ab-cdef-0123-456789abcdef Download book and name the file automatically
  30. kobo-book-downloader get /dir/ --all Download all your books
  31. kobo-book-downloader info Show the location of the program's configuration file
  32. kobo-book-downloader list List your unread books
  33. kobo-book-downloader list --all List all your books
  34. kobo-book-downloader list --help Get additional help for the list command (it works for get too)"""
  35. print( usage )
  36. @staticmethod
  37. def __GetBookAuthor( book: dict ) -> str:
  38. contributors = book.get( "ContributorRoles" )
  39. authors = []
  40. for contributor in contributors:
  41. role = contributor.get( "Role" )
  42. if role == "Author":
  43. authors.append( contributor[ "Name" ] )
  44. # Unfortunately the role field is not filled out in the data returned by the "library_sync" endpoint, so we only
  45. # use the first author and hope for the best. Otherwise we would get non-main authors too. For example Christopher
  46. # Buckley beside Joseph Heller for the -- terrible -- novel Catch-22.
  47. if len( authors ) == 0 and len( contributors ) > 0:
  48. authors.append( contributors[ 0 ][ "Name" ] )
  49. return " & ".join( authors )
  50. @staticmethod
  51. def __SanitizeFileName( fileName: str ) -> str:
  52. result = ""
  53. for c in fileName:
  54. if c.isalnum() or " ,;.!(){}[]#$'-+@_".find( c ) >= 0:
  55. result += c
  56. result = result.strip( " ." )
  57. result = result[ :100 ] # Limit the length -- mostly because of Windows. It would be better to do it on the full path using MAX_PATH.
  58. return result
  59. @staticmethod
  60. def __MakeFileNameForBook( book: dict ) -> str:
  61. fileName = ""
  62. author = Commands.__GetBookAuthor( book )
  63. if len( author ) > 0:
  64. fileName = author + " - "
  65. fileName += book[ "Title" ]
  66. fileName = Commands.__SanitizeFileName( fileName )
  67. fileName += ".epub"
  68. return fileName
  69. @staticmethod
  70. def __GetBook( revisionId: str, outputPath: str ) -> None:
  71. if os.path.isdir( outputPath ):
  72. book = Globals.Kobo.GetBookInfo( revisionId )
  73. fileName = Commands.__MakeFileNameForBook( book )
  74. outputPath = os.path.join( outputPath, fileName )
  75. else:
  76. parentPath = os.path.dirname( outputPath )
  77. if not os.path.isdir( parentPath ):
  78. raise KoboException( "The parent directory ('%s') of the output file must exist." % parentPath )
  79. print( "Downloading book to '%s'." % outputPath )
  80. Globals.Kobo.Download( revisionId, Kobo.DisplayProfile, outputPath )
  81. @staticmethod
  82. def __GetAllBooks( outputPath: str ) -> None:
  83. if not os.path.isdir( outputPath ):
  84. raise KoboException( "The output path must be a directory when downloading all books." )
  85. bookList = Globals.Kobo.GetMyBookList()
  86. for entitlement in bookList:
  87. newEntitlement = entitlement.get( "NewEntitlement" )
  88. if newEntitlement is None:
  89. continue
  90. bookMetadata = newEntitlement[ "BookMetadata" ]
  91. fileName = Commands.__MakeFileNameForBook( bookMetadata )
  92. outputFilePath = os.path.join( outputPath, fileName )
  93. print( "Downloading book to '%s'." % outputFilePath )
  94. Globals.Kobo.Download( bookMetadata[ "RevisionId" ], Kobo.DisplayProfile, outputFilePath )
  95. @staticmethod
  96. def GetBookOrBooks( revisionId: str, outputPath: str, getAll: bool ) -> None:
  97. revisionIdIsSet = ( revisionId is not None ) and len( revisionId ) > 0
  98. if getAll:
  99. if revisionIdIsSet:
  100. raise KoboException( "Got unexpected book identifier parameter ('%s')." % revisionId )
  101. Commands.__GetAllBooks( outputPath )
  102. else:
  103. if not revisionIdIsSet:
  104. raise KoboException( "Missing book identifier parameter. Did you mean to use the --all parameter?" )
  105. Commands.__GetBook( revisionId, outputPath )
  106. @staticmethod
  107. def __IsBookRead( newEntitlement: dict ) -> bool:
  108. readingState = newEntitlement.get( "ReadingState" )
  109. if readingState is None:
  110. return False
  111. statusInfo = readingState.get( "StatusInfo" )
  112. if statusInfo is None:
  113. return False
  114. status = statusInfo.get( "Status" )
  115. return status == "Finished"
  116. @staticmethod
  117. def ListBooks( listAll: bool ) -> None:
  118. colorama.init()
  119. bookList = Globals.Kobo.GetMyBookList()
  120. rows = []
  121. for entitlement in bookList:
  122. newEntitlement = entitlement.get( "NewEntitlement" )
  123. if newEntitlement is None:
  124. continue
  125. if ( not listAll ) and Commands.__IsBookRead( newEntitlement ):
  126. continue
  127. bookMetadata = newEntitlement[ "BookMetadata" ]
  128. rows.append( [ bookMetadata[ "RevisionId" ], bookMetadata[ "Title" ], Commands.__GetBookAuthor( bookMetadata ) ] )
  129. rows = sorted( rows, key = lambda columns: columns[ 1 ] )
  130. for columns in rows:
  131. revisionId = colorama.Style.DIM + columns[ 0 ] + colorama.Style.RESET_ALL
  132. title = colorama.Style.BRIGHT + columns[ 1 ] + colorama.Style.RESET_ALL
  133. author = columns[ 2 ]
  134. if len( author ) > 0:
  135. print( "%s \t %s by %s" % ( revisionId, title, author ) )
  136. else:
  137. print( "%s \t %s" % ( revisionId, title ) )
  138. @staticmethod
  139. def Info():
  140. print( "The configuration file is located at:\n%s" % Globals.Settings.SettingsFilePath )