You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

239 lines
8.4 KiB

7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
  1. package bandcampcollectiondownloader
  2. import com.google.gson.Gson
  3. import com.google.gson.annotations.SerializedName
  4. import org.jsoup.Jsoup
  5. import org.zeroturnaround.zip.ZipUtil
  6. import java.io.FileOutputStream
  7. import java.net.HttpURLConnection
  8. import java.net.URL
  9. import java.nio.file.Files
  10. import java.nio.file.Path
  11. import java.nio.file.Paths
  12. import java.util.*
  13. import java.util.regex.Pattern
  14. import javax.mail.internet.ContentDisposition
  15. data class ParsedCookie(
  16. @SerializedName("Name raw")
  17. val nameRaw: String,
  18. @SerializedName("Content raw")
  19. val contentRaw: String
  20. )
  21. data class ParsedBandcampData(
  22. val digital_items: Array<DigitalItem>
  23. )
  24. data class DigitalItem(
  25. val downloads: Map<String, Map<String, String>>,
  26. val package_release_date: String,
  27. val title: String,
  28. val artist: String,
  29. val download_type: String,
  30. val art_id: String
  31. )
  32. data class ParsedStatDownload(
  33. val download_url: String
  34. )
  35. fun parsedCookiesToMap(parsedCookies: Array<ParsedCookie>): Map<String, String> {
  36. val result = HashMap<String, String>()
  37. for (parsedCookie in parsedCookies) {
  38. result.put(parsedCookie.nameRaw, parsedCookie.contentRaw)
  39. }
  40. return result
  41. }
  42. const val BUFFER_SIZE = 4096
  43. /**
  44. * From http://www.codejava.net/java-se/networking/use-httpurlconnection-to-download-file-from-an-http-url
  45. */
  46. fun downloadFile(fileURL: String, saveDir: Path, optionalFileName: String = ""): Path {
  47. val url = URL(fileURL)
  48. val httpConn = url.openConnection() as HttpURLConnection
  49. val responseCode = httpConn.getResponseCode()
  50. // always check HTTP response code first
  51. if (responseCode == HttpURLConnection.HTTP_OK) {
  52. val disposition = httpConn.getHeaderField("Content-Disposition")
  53. val fileName: String =
  54. if (optionalFileName != "") {
  55. optionalFileName
  56. } else if (disposition != null) {
  57. val parsedDisposition = ContentDisposition(disposition)
  58. parsedDisposition.getParameter("filename")
  59. } else {
  60. Paths.get(url.file).fileName.toString()
  61. }
  62. // opens input stream from the HTTP connection
  63. val inputStream = httpConn.getInputStream()
  64. val saveFilePath = saveDir.resolve(fileName)
  65. val saveFilePathString = saveFilePath.toAbsolutePath().toString()
  66. // opens an output stream to save into file
  67. val outputStream = FileOutputStream(saveFilePathString)
  68. val buffer = ByteArray(BUFFER_SIZE)
  69. var bytesRead = inputStream.read(buffer)
  70. while (bytesRead != -1) {
  71. outputStream.write(buffer, 0, bytesRead)
  72. bytesRead = inputStream.read(buffer)
  73. }
  74. outputStream.close()
  75. inputStream.close()
  76. httpConn.disconnect()
  77. return saveFilePath
  78. } else {
  79. throw Exception("No file to download. Server replied HTTP code: $responseCode")
  80. }
  81. }
  82. /**
  83. * Core function called from the main
  84. */
  85. fun downloadAll(cookiesFile: String, bandcampUser: String, downloadFormat: String, downloadFolder: Path) {
  86. // Parse JSON cookies (obtained with "Cookie Quick Manager" Firefox addon)
  87. val gson = Gson()
  88. val jsonData = String(Files.readAllBytes(Paths.get(cookiesFile)))
  89. val parsedCookies = gson.fromJson(jsonData, Array<ParsedCookie>::class.java)
  90. val cookies = parsedCookiesToMap(parsedCookies)
  91. // Get collection page with cookies, hence with download links
  92. val doc = Jsoup.connect("https://bandcamp.com/$bandcampUser")
  93. .cookies(cookies)
  94. .get()
  95. println("""Found collection page: "${doc.title()}"""")
  96. if (!doc.toString().contains("buy-now")) {
  97. println("Provided cookies appear to be working!")
  98. }
  99. // Get download pages
  100. val collection = doc.select("span.redownload-item a")
  101. // For each download page
  102. for (item in collection) {
  103. val downloadPageURL = item.attr("href")
  104. val downloadPageJsonParsed = getDataBlobFromDownloadPage(downloadPageURL, cookies, gson)
  105. // Extract data from blob
  106. val digitalItem = downloadPageJsonParsed.digital_items.get(0)
  107. val albumtitle = digitalItem.title
  108. val artist = digitalItem.artist
  109. val releaseDate = digitalItem.package_release_date
  110. val releaseYear = releaseDate.subSequence(7, 11)
  111. val isSingleTrack: Boolean = digitalItem.download_type == "t"
  112. val url = digitalItem.downloads.get(downloadFormat)?.get("url").orEmpty()
  113. val artid = digitalItem.art_id
  114. // Prepare artist and album folder
  115. val albumFolderName = "$releaseYear - $albumtitle"
  116. val artistFolderPath = Paths.get("$downloadFolder").resolve(artist)
  117. val albumFolderPath = artistFolderPath.resolve(albumFolderName)
  118. downloadAlbum(artistFolderPath, albumFolderPath, albumtitle, url, cookies, gson, isSingleTrack, artid)
  119. }
  120. }
  121. fun downloadAlbum(artistFolderPath: Path?, albumFolderPath: Path, albumtitle: String, url: String, cookies: Map<String, String>, gson: Gson, isSingleTrack: Boolean, artid: String) {
  122. // If the artist folder does not exist, we create it
  123. if (!Files.exists(artistFolderPath)) {
  124. Files.createDirectory(artistFolderPath)
  125. }
  126. // If the album folder does not exist, we create it
  127. if (!Files.exists(albumFolderPath)) {
  128. Files.createDirectory(albumFolderPath)
  129. }
  130. // If the folder is empty, or if it only contains the zip.part file, we proceed
  131. val amountFiles = albumFolderPath.toFile().listFiles().size
  132. if (amountFiles < 2) {
  133. val outputFilePath: Path = prepareDownload(albumtitle, url, cookies, gson, albumFolderPath)
  134. // If this is a zip, we unzip
  135. if (!isSingleTrack) {
  136. // Unzip
  137. try {
  138. ZipUtil.unpack(outputFilePath.toFile(), albumFolderPath.toFile())
  139. } finally {
  140. // Delete zip
  141. Files.delete(outputFilePath)
  142. }
  143. }
  144. // Else if this is a single track, we just fetch the cover
  145. else {
  146. val coverURL = "https://f4.bcbits.com/img/a${artid}_10"
  147. println("Downloading cover ($coverURL)...")
  148. downloadFile(coverURL, albumFolderPath, "cover.jpg")
  149. }
  150. println("done.")
  151. } else {
  152. println("Album $albumtitle already done, skipping")
  153. }
  154. }
  155. fun getDataBlobFromDownloadPage(downloadPageURL: String?, cookies: Map<String, String>, gson: Gson): ParsedBandcampData {
  156. println("Analyzing download page $downloadPageURL")
  157. // Get page content
  158. val downloadPage = Jsoup.connect(downloadPageURL)
  159. .cookies(cookies)
  160. .timeout(100000).get()
  161. // Get data blob
  162. val downloadPageJson = downloadPage.select("#pagedata").attr("data-blob")
  163. val downloadPageJsonParsed = gson.fromJson(downloadPageJson, ParsedBandcampData::class.java)
  164. return downloadPageJsonParsed
  165. }
  166. fun prepareDownload(albumtitle: String, url: String, cookies: Map<String, String>, gson: Gson, albumFolderPath: Path): Path {
  167. println("Preparing download of $albumtitle ($url)...")
  168. val random = Random()
  169. // Construct statdownload request URL
  170. val statdownloadURL: String = url
  171. .replace("/download/", "/statdownload/")
  172. .replace("http", "https") + "&.vrs=1" + "&.rand=" + random.nextInt()
  173. // Get statdownload JSON
  174. println("Getting download link ($statdownloadURL)")
  175. val statedownloadUglyBody: String = Jsoup.connect(statdownloadURL)
  176. .cookies(cookies)
  177. .timeout(100000)
  178. .get().body().select("body").get(0).text().toString()
  179. val prefixPattern = Pattern.compile("""if\s*\(\s*window\.Downloads\s*\)\s*\{\s*Downloads\.statResult\s*\(\s*""")
  180. val suffixPattern = Pattern.compile("""\s*\)\s*};""")
  181. val statdownloadJSON: String =
  182. prefixPattern.matcher(
  183. suffixPattern.matcher(statedownloadUglyBody)
  184. .replaceAll("")
  185. ).replaceAll("")
  186. // Parse statdownload JSON and get real download URL, and retrieve url
  187. val statdownloadParsed: ParsedStatDownload = gson.fromJson(statdownloadJSON, ParsedStatDownload::class.java)
  188. val realDownloadURL = statdownloadParsed.download_url
  189. println("Downloading $albumtitle ($realDownloadURL)")
  190. // Download content
  191. val outputFilePath: Path = downloadFile(realDownloadURL, albumFolderPath)
  192. return outputFilePath
  193. }