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

Zunä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

Die 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: 2. Januar, 2015
Tags: , , , , ,

Weitere interessante Beiträge

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 »


iOS-Android-Converter

Lange Zeit habe ich bei neuen App-Projekten zunächst die Sprachdateien / Language-Files für iOS geschrieben, und diese dann für die Android-Version händisch umgeschrieben. Das war vom Aufwand her immer schnell gemacht solange die Anzahl der implementierten Sprachen und der Umfang der Textbausteine niedrig war. Abgesehen vom Zeitaufwand ist das ganz schön langweilige und stumpfe Arbeit, [...] Weiterlesen »


Screen Recorder / Capture Software selber programmieren

Auf der Suche nach einem einfach zu bedienenen Screen-Recorder-Tool bin ich über allerhand kostenloser sowie kostenpflichtiger Softwarelösungen gestolpert. Aus meiner Sicht waren ziemlich alle entweder völlig überladen oder auch schlecht bzw. kompliziert zu bedienen. Andere waren auch einfach nur teuer im Verhältnis zum Funktionsumfang. Mein Fazit: Selber programmieren! Screen-Recorder-Tool selber programmieren Dabei brauche ich lediglich [...] Weiterlesen »


ESP8266-WiFi-Modul flashen

Da ich mich seit kurzem wieder intensiver mit den Themen Smart Home und "Home automation" beschäftige habe ich beschlossen einige meiner Baustellen, Stolpersteine und Lösungen hier zu veröffentlichen. Wie im Beitrag zu meinem Smart-Home-Project V1 bereits erwähnt setze ich auf das WLAN-Modul ESP8266-ESP01 , da dieses unter anderem klein, zuverlässig und günstig ist. In diesem [...] Weiterlesen »


Fitnessboard v1 - DIYS

Am Wochenende hatte ich glücklicherweise mal wieder etwas Zeit zum Basteln und dazu noch eine ganz nette Idee für ein Fitness-Board für das Training zuhause, welches aus der Liegestütz-Position bedient wird. Ein Video dazu gibt es weiter unten in diesem Artikel und im Youtube-Channel von tedokai.de. Jeder der Zuhause dann und wann Sport betreibt kennt [...] Weiterlesen »