iTunes-Connect - Sales-Reports

Amazon

Wer Apple und iTunes-Connect (ITC) kennt, weiß von den Problemchen welche ITC teilweise mit sich bringt. Mich persönlich hat immer gestört, das die Sales-Reports / Verkaufszahlen des Vortages manchmal erst am späten Abend des Folgetages verfügbar waren. Das liegt wohl daran, das die Apple-Server die Reports für jeden Entwickler aufbereiten müssen und es hier öfter mal zu Engpässen an Rechenressourcen kommt. Das ist natürlich nur eine Vermutung meinerseits...Wie dem auch sei! ;-)

itc_umsatz_rechnerIch habe mir vor ca. einem Jahr ein kleines Tool in Java namens "ITC - Umsatz-Rechner" (ja ja, ich weiß, ziemlich einfallsreich ;) ) geschrieben, welches die Roh-Daten der Verkäufe für einen beliebigen Zeitraum mit Hilfe der "Autoingestion.class" (Download über Entwickler-Account bei Apple) herunter lädt und diese Daten auswertet. Heraus kommt dann eine HTML-Datei, welche alle Verkaufszahlen aller Apps des gewählten Zeitraumes enthält. Häufig vorkommende Währungen wie USD, GBP, AUD, CAD und z.B. CHF werden anhand eines hinterlegten Wechselkurses direkt in Euro (EUR) umgerechnet. Des weiteren werden Zusammenfassung für die einzelnen Länder erstellt, woraus hervor geht, in welchem Land wieviele Neuinstallationen, Updates oder InApp-Käufe getätigt wurden. Das Ganze dann nochmal separiert nach iPhone, iPad oder Mac.

ITC - Umsatz-Rechner - Funktionsweise

amz box.jpgZunächst holen wir uns eine Kalendar-Instanz damit wir Apples Autoingestion.class mitteilen können, welchen Zeitraum oder welches Datum wir gerne hätten.

Calendar cal = Calendar.getInstance();
String sDateType, sDateFormat;

if (!bLoadTagesansicht){
//monatlich
sDateType = "Monthly";
sDateFormat = "yyyyMM";
cal.add(Calendar.MONTH, -1);
}else{
//täglich
sDateType = "Daily";
sDateFormat = "yyyyMMdd";
cal.add(Calendar.DATE, iHeuteMinusXTage);
/*
//jährlich
sDateType = "Yearly";
sDateFormat = "yyyy";
cal.add(Calendar.YEAR, -1);
*/
}

String sDate = new SimpleDateFormat(sDateFormat).format(cal.getTime());
String sDateTypeShort = sDateType.substring(0, 1);

Jetzt bilden wir die entsprechende Befehlszeile für die Autoingestion.class und rufen diese mittels exec() auf und warten 2-3 Sekunden bis der Download abgeschlossen ist.

String sCmd = "java Autoingestion"
+ " " + sUser
+ " " + sPass
+ " " + sVendorID
+ " Sales "
+ sDateType
+ " Summary"
+ " " + sDate;

Process proc = Runtime.getRuntime().exec(sCmd);

InputStream inputStream = proc.getInputStream();
InputStreamReader inputStreamReader = new InputStreamReader(inputStream);
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);

proc.waitFor();

String line;
while ((line = bufferedReader.readLine()) != null)
{
//echo(line);
}

Nachdem die Roh-Daten aus dem ITC-Account herunter geladen wurden, kann anhand des gewählten Zeitraumes und der Vendor-ID der Dateiname der herunter geladenen Datei gebildet werden. Diese ist mit GZip komprimiert und wird daher im nächsten Step entpackt, und im Anschluß geparsed bzw. verarbeitet. Hinweis: Die verwendeten Hilfsklassen wie Downloader.java und myAppUmsatzEntry.java werden weiter unten näher erläutert. Ist an dieser Stelle erstmal nicht ganz so wichtig.

String sFile = "S_" + sDateTypeShort + "_" + sVendorID + "_" + sDate + ".txt.gz";
//echo("Suche " + sFile + "...");

String sDataFile = "data.txt";
if (downloader.unzip(sFile, sDataFile)){
calculate(sDataFile, sSaveAs);
}else{
//echo("Daten fuer " + sDate + " liegen noch nicht vor. :/");
}

Die Funktion calculate() ließt die Datei mit den Rohdaten aus ITC ein, öffnet ein neues HTML-Dokument und wandelt die Daten kurz gesagt in etwas schöneres, aber vor allem leserlicheres um. Zunächst wird eine ArrayList erstellt, in welche die einzelnen Daten geschrieben werden.

ArrayList<myAppUmsatzEntry> umsaetze = new ArrayList<myAppUmsatzEntry>();

Dann wird die Datei mit den Rohdaten eingelesen, die entsprechenden Spalten und Daten-Abschnitte zugeordnet und im Array zwischengespeichert.

FileReader fr = new FileReader(sFile);
BufferedReader br = new BufferedReader(fr);

String[] arr;
String s = br.readLine(); //erste zeile überspringen//head
while ((s = br.readLine())!=null)
{
arr = s.split("\t");
if (arr.length>=20)
{
myAppUmsatzEntry umsatzEntry = new myAppUmsatzEntry();

umsatzEntry.sSKU = arr[2];
umsatzEntry.sVersion = arr[5];
umsatzEntry.sType = arr[6];
umsatzEntry.iMenge = Integer.parseInt(arr[7]);
umsatzEntry.fGewinn = Float.parseFloat(arr[8]);
umsatzEntry.sWaehrung = arr[11];
umsatzEntry.sLand = arr[12];

if (sDatumVon.equalsIgnoreCase(""))
{
sDatumVon = arr[9];
sDatumBis = arr[10];

if (sDatumVon.equalsIgnoreCase(sDatumBis))
bIsTagesansicht = true;

downloader.echo("Statistik: " + sDatumVon + " bis " + sDatumBis + "\n");
}

//zwischenspeichern
umsaetze.add(umsatzEntry);
}
}
br.close();

Jetzt wo alle Daten formatiert im Arbeitsspeicher bzw. der ArrayList liegen, sortieren wir diese noch nach der SKU, damit alle Einträge mit der jeweiligen App-Kennung zusammen liegen.

Collections.sort(umsaetze, new SortBySku());

Nun wird eine neue HTML-Datei geöffnet, und als erstes eine Überschrift definiert, welches das aktuelle Datum bzw. den gewählten Zeitraum der Daten beinhaltet.

String sOutputFile = sSaveAs; //sDatumVon + "_bis_" + sDatumBis + ".html";
sOutputFile = sOutputFile.replace("/", "_");

PrintWriter pWriter = new PrintWriter(new FileWriter(sOutputFile), true);
pWriter.println(getHtmlStart() + "<h1>" + sDatumVon + " bis " + sDatumBis + "</h1>" + nl + nl);

Nun werden die einzelnen Einträge der ArrayList abgearbeitet, und der entsprechende Output erzeugt. Hier werden die Wechselkurse mit den Verkäufen in den entsprechenden Ländern verrechnet und formatiert ausgegeben.

.....................

pWriter.println(nl + "</div><b>" + sLastSKU + " - Installationen: " + String.valueOf(iInstallLastSku) + " - Updates: " + String.valueOf(iUpdateLastSku) + "</b>" + nl
+ "Gewinn:"
+ " " + downloader.getPreisFormated(fEuroLastSku) + " EUR"
+ " + " + downloader.getPreisFormated(fUsdLastSku) + " USD"
+ " + " + downloader.getPreisFormated(fGbpLastSku) + " GBP"
+ " + " + downloader.getPreisFormated(fAudLastSku) + " AUD"
+ " + " + downloader.getPreisFormated(fCadLastSku) + " CAD"
+ " + " + downloader.getPreisFormated(fChfLastSku) + " CHF"
+ " + X (andere W&auml;hrungen)"
+ nl);

float fTmp = fEuroLastSku
+ (fUsdLastSku * getWechselkurs("usd"))
+ (fGbpLastSku * getWechselkurs("gbp"))
+ (fAudLastSku * getWechselkurs("aud"))
+ (fCadLastSku * getWechselkurs("cad"))
+ (fChfLastSku * getWechselkurs("chf"));
pWriter.println("<h3>Gewinn EUR - kalkuliert: " + downloader.getPreisFormated(fTmp) + " EUR</h3>" + nl);

.....................

An dieser Stelle spare ich mir einige Code-Abschnitte, einfach um diese Seite hier nicht noch weiter zu flooten :D Den gesamten Code als Download gibt es weiter unten auf der Seite.

Helferklasse - Downloader.java

amz box.jpgDie Klasse Downloader enthält diverse Hilfsfunktionen. Unter anderem auch Download- und Rundungsfunktionen. Zum entpacken des GZip-Archives nutze ich eine Funktion namens unzip(String sFile, String sSaveAS).

Der erste Parameter ist der Pfad zum GZip-Archiv, und der zweite Parameter beinhaltet den Pfad bzw. Dateinamen, unter welchem die entpackte Datei gespeichert werden soll.

public static boolean unzip(String sFile, String sSaveAS)
{
try
{
File f = new File(sFile);
if (!f.exists())
return false;

GZIPInputStream gzipInputStream = null;
gzipInputStream = new GZIPInputStream(new FileInputStream(sFile));
OutputStream out = new FileOutputStream(sSaveAS);

byte[] buf = new byte[1024];
int len;
while ((len = gzipInputStream.read(buf)) > 0) {
out.write(buf, 0, len);
}
gzipInputStream.close();
out.close();
f.delete();

return true;
}
catch (Exception ex)
{
echo("error unzip: " + ex.getLocalizedMessage());
}
return false;
}

Helferklasse - myAppUmsatzEntry.java

Die Klasse myAppUmsatzEntry ist eine Struktur die Informationen zur App, Version, dem entsprechenden Land, Anzahl Verkäufe bzw. Downloads, dem Gewinn und einigem mehr enthält. Neben dem Speichern von Informationen gibt es auch noch eine Funktion namens getTypeAsString() welche anhand des Type-Codes des Eintrages einen lesbaren String zurück gibt. So muss man nicht schauen das 7T ein Update auf iPad war. Hier die Funktion auf einen Blick, sollte selbsterklärend sein. ;-)

public String getTypeAsString()
{
if (sType.equalsIgnoreCase("1"))
return "Free/paid";
else if (sType.equalsIgnoreCase("7"))
return "Update";
else if (sType.equalsIgnoreCase("IA1") || sType.equalsIgnoreCase("IA9") || sType.equalsIgnoreCase("IAY") || sType.equalsIgnoreCase("IAC"))
return "InApp";
else if (sType.equalsIgnoreCase("1F"))
return "Free/paid";
else if (sType.equalsIgnoreCase("7F"))
return "Update";
else if (sType.equalsIgnoreCase("1T"))
return "Free/paid (iPad)";
else if (sType.equalsIgnoreCase("7T"))
return "Update (iPad)";
else if (sType.equalsIgnoreCase("F1"))
return "Free/paid (Mac)";
else if (sType.equalsIgnoreCase("F7"))
return "Update (Mac)";
else if (sType.equalsIgnoreCase("FI1"))
return "InApp (Mac)";
else if (sType.equalsIgnoreCase("1E"))
return "Paid (iPhone/iPod)";
else if (sType.equalsIgnoreCase("1EP"))
return "Paid (iPad)";
else if (sType.equalsIgnoreCase("1EU"))
return "Paid (Universal)";
else
return "Unknown";
}

Schlußwort und Download-Links zum Code

Die Idee hinter diesem Tool ist simpel, die Umsetzung relativ gut gelungen. Da das Tool nebenbei und auf die schnelle geschrieben wurde, könnte hier und da der Code sicher noch etwas optimiert werden, aber alles in allem Funktioniert es wie es soll.

Hier gibt es die entsprechenden Klassen und den sonst noch benötigten Code zum Download.
Viel Spaß damit! :-)

Bei Fragen, Anmerkungen oder Erweiterungsvorschlägen nutze bitte das Kontaktformular.

Zuletzt bearbeitet: 23. Mai, 2019
Tags: , , , , ,

Dies könnte dich auch interessieren

Gitarrenhalter für die Wand aus dem 3D-Drucker

Gitarrenhalter für die Wand oder auch Gitarrenständer sind eine schöne Sache, insbesondere wenn sich die Sammlung an Musikinstrumenten über die Jahre vergrößert. Mir persönlich gefallen Wandhalterungen ganz gut, da Sie nicht nur elegant Stauraum schaffen, sondern gleichzeitig auch ein echter Hingucker sind. Dem Raum verleihen Akustikgitarre, E-Gitarre, Bass, Banjo oder Ukulele einen gewissen Charakter wenn [...] Weiterlesen »


Der Apfel-Appstore-Reader

Aufgrund der Tatsache das Apple sich bezüglich der App-Rankings (Positionierung im Appstore) sehr gedeckt hält, und auch im Report-Tool (ITC / iTunes-Connect -> Sales & Trends) keinerlei Informationen auftauchen, an welcher Position man sich mit der jeweiligen App in z.B. einer Kategorie wie "Dienstprogramme" befindet, habe ich ein kleines Java-Tool (mit dem einfallsreichen Name Appstore-Reader) [...] Weiterlesen »


Telegram Messenger und die Bot API - Teil 2

Im ersten Teil - Telegram Messenger und die Bot API - bin ich auf einige Grundlagen eingegangen wie man einen Telegram-Bot einrichtet und diesen nutzt um sich Nachrichten, Informationen und sonstiges bequem auf sein Handy zu schicken. Da sich dieses Werkzeug als äußerst praktisch erwiesen hat, habe ich mich dazu entschlossen den vorherigen Beitrag nochmal [...] Weiterlesen »


Color-Calculator / Farbrechner

Der Color-Calculator errechnet aus Hex-Farbwerten die entsprechenden Dezimal- und Float-Werte. So kann man Beispielsweise schnell und einfach Farbewerte aus dem Web für seine iOS-App übernehmen, ohne manuell umrechnen zu müssen. Für alle Tippfaulen unter euch (zu denen ich mich im übrigen auch zähle ;-) ) wird der passende Objective-C Programmcode generiert. Die Eingabe von ganzen [...] Weiterlesen »


Apfel-Appstore-Reader - Update 2.0

Wie im Artikel - Apfel-Appstore-Reader - beschrieben, hält sich Apple bezüglich der App-Rankings (Positionierung im Appstore) sehr gedeckt. Auch im Report-Tool (ITC / iTunes-Connect -> Sales & Trends) keinerlei Informationen auftauchen, an welcher Position man sich mit der jeweiligen App in z.B. einer Kategorie wie “Produktivität” befindet, habe ich vor längerer Zeit ein kleines Java-Tool [...] Weiterlesen »