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

christ schmuck.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 music.pngDie 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

Privacy Browser für mehr Sicherheit und Privatsphäre

Mehr Datenschutz, Sicherheit und vor allem mehr Privatsphäre im Internet wird von allen Seiten gefordert, und das nicht erst nach dem Inkrafttreten der EU-weiten Datenschutz-Grundverordnung (DSGVO / Datenschutz ). Generell kann man diesen Wunsch durchaus nachvollziehen, denn wer möchte schon freiwillig großen IT-Konzernen erlauben die eigenen Daten weiterzugeben, zu verkaufen und wer weiß was damit [...] Weiterlesen »


HTAccess-Generator

Erstelle dir hier schnell und einfach einen HTAccess-Verzeichnisschutz für deine Web-/Internetseite. So kannst du den Hypertext-Zugriff auf Verzeichnisse deiner Webseite vor unberechtigten Zugriffen schützen. Der jeweilige Benutzer muss so zunächst die Benutzerkennung und das entsprechende Passwort eingeben bevor er den Inhalt zu Gesicht bekommt. Zum Beispiel lässt sich so auch ein CMS (Content-Management-Systeme wie z.B. [...] 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 »


Noch mehr nützliche Dinge aus dem 3D Drucker

Hier und heute nun die Fortsetzung aus dem ersten Teil, über Sinn und Unsinn aus dem 3D-Drucker. Über Nutzen und Gebrauchstauglichkeit lässt sich ja streiten, aber beispielsweise so manch praktischer Küchenhelfer lässt sich nicht weg diskutieren. Was tut man nur, wenn man zum Beispiel Wiener Schnitzel zubereiten will, aber der Fleischklopfer aus Holz sich soeben [...] Weiterlesen »


DynDNS-Alternative im Eigenbau

Wer ohnehin über einen eigenen Webserver/Webspace im Internet und etwas Programmierkenntnisse verfügt, kann sich ohne großen Aufwand einen Service wie DynDNS.org & Co. sparen, will man auf Teile seines Heimnetzwerkes von Unterwegs aus zugreifen. Solche Services stellen einen Hostnamen/URL bereit (z.B. mein-heimnetzwerk.dyndns.org) welche dann auf die IP-Adresse des Heimnetzwerkes weitergeleitet wird. Neben einem oft kostenpflichtigem [...] Weiterlesen »